[Compose] Breaking Change: Major Compose API Refactor (#1827)

This PR is a major update to the Lottie Compose APIs.
Notable changes:
* LottieAnimation composable now just takes a composition, progress as a Float, and base properties such as ImageAssetDelegate, mergePaths, etc.
* Animating the composition can be done either with a manual animation, gesture, etc or via the new animateLottieComposition() function or LottieAnimatable class.
* LottieAnimation contains overloads that make it easier to just pass in a LottieCompositionSpec or animation parameters and under the hood, it will wrap lottieComposition() and animateLottieComposition().

diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 898f7d6..8793d22 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -6,6 +6,7 @@
       <option name="ANNOTATION_PARAMETER_WRAP" value="1" />
     </JavaCodeStyleSettings>
     <JetCodeStyleSettings>
+      <option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
       <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
     </JetCodeStyleSettings>
     <XML>
diff --git a/CHANGELOG_COMPOSE.md b/CHANGELOG_COMPOSE.md
index c20b92e..2e6fe0d 100644
--- a/CHANGELOG_COMPOSE.md
+++ b/CHANGELOG_COMPOSE.md
@@ -1,6 +1,22 @@
 #### Note: For the time being, we won't provide numbered releases for every new Jetpack Compose
 version. Check out our [snapshot builds](https://github.com/airbnb/lottie/blob/master/android-compose.md#getting-started) instead.
 
+# 1.0.0-beta09-1
+## Breaking Changes
+`LottieAnimation` now takes a progress float instead of driving the animation internally.
+The driving of animations has been split into a new `LottieAnimatable` class and
+`animateLottieCompositionAsState` function. These are analogous to Jetpack's `Animatable` and
+`animate*AsState` functions.
+Properties that pertain to the animation such as speed, repeat count, and the new clip spec are part of
+`animateLottieComposition` whereas properties that are related to rendering such as enabling merge paths
+and setting an image asset delegate are on the `LottieAnimation` composable.
+
+`lottieComposition` has also been renamed `rememberLottieComposition`.
+
+There are overloaded version of `LottieAnimation` that merge the properties for convenience. Please
+refer to the docs for `LottieAnimation`, `LottieAnimatable`, `animateLottieCompositionAsState`
+and `rememberLottieComposition` for more information.
+
 # 1.0.0-beta07-1
 * Compatible with Jetpack Compose Beta 07
 
diff --git a/build.gradle b/build.gradle
index b68b74f..71d67e0 100644
--- a/build.gradle
+++ b/build.gradle
@@ -19,8 +19,8 @@
     classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
     classpath 'org.ajoberstar:grgit:1.9.3'
     classpath "net.ltgt.gradle:gradle-errorprone-plugin:2.0.1"
-    classpath 'com.vanniktech:gradle-maven-publish-plugin:0.13.0'
-    classpath 'org.jetbrains.dokka:dokka-gradle-plugin:1.4.10.2'
+    classpath 'com.vanniktech:gradle-maven-publish-plugin:0.14.2'
+    classpath 'org.jetbrains.dokka:dokka-gradle-plugin:1.4.30'
   }
 }
 
diff --git a/issue-repro-compose/src/main/java/com/airbnb/lottie/issues/compose/ComposeIssueReproActivity.kt b/issue-repro-compose/src/main/java/com/airbnb/lottie/issues/compose/ComposeIssueReproActivity.kt
index 4e55a5d..8be7519 100755
--- a/issue-repro-compose/src/main/java/com/airbnb/lottie/issues/compose/ComposeIssueReproActivity.kt
+++ b/issue-repro-compose/src/main/java/com/airbnb/lottie/issues/compose/ComposeIssueReproActivity.kt
@@ -4,6 +4,7 @@
 import androidx.activity.compose.setContent
 import androidx.appcompat.app.AppCompatActivity
 import androidx.compose.runtime.Composable
+import com.airbnb.lottie.compose.LottieAnimation
 
 class ComposeIssueReproActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
@@ -15,5 +16,6 @@
 
     @Composable
     fun Content() {
+        LottieAnimation(null)
     }
 }
diff --git a/lottie-compose/build.gradle b/lottie-compose/build.gradle
index 616f72a..422498c 100644
--- a/lottie-compose/build.gradle
+++ b/lottie-compose/build.gradle
@@ -26,7 +26,11 @@
   }
   kotlinOptions {
     jvmTarget = '1.8'
-    freeCompilerArgs += ["-Xallow-jvm-ir-dependencies", "-Xskip-prerelease-check"]
+    freeCompilerArgs += [
+        "-Xallow-jvm-ir-dependencies",
+        "-Xskip-prerelease-check",
+        "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
+    ]
   }
   buildFeatures {
     compose true
@@ -49,4 +53,8 @@
   api project(':lottie')
   implementation "androidx.compose.foundation:foundation:$composeVersion"
   implementation "androidx.compose.ui:ui:$composeVersion"
+  testImplementation 'org.robolectric:robolectric:4.4'
+  testImplementation 'androidx.collection:collection-ktx:1.1.0'
+  testImplementation 'junit:junit:4.13.2'
+  testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.3'
 }
\ No newline at end of file
diff --git a/lottie-compose/gradle.properties b/lottie-compose/gradle.properties
index ee86ce8..9a30505 100644
--- a/lottie-compose/gradle.properties
+++ b/lottie-compose/gradle.properties
@@ -4,4 +4,4 @@
 POM_ARTIFACT_ID=lottie-compose
 POM_PACKAGING=aar
 GROUP=com.airbnb.android
-VERSION_NAME=1.0.0-beta07-2-SNAPSHOT
+VERSION_NAME=1.0.0-beta09-1-SNAPSHOT
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimatable.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimatable.kt
new file mode 100644
index 0000000..db1a8bc
--- /dev/null
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimatable.kt
@@ -0,0 +1,293 @@
+package com.airbnb.lottie.compose
+
+import androidx.compose.animation.core.AnimationConstants
+import androidx.compose.foundation.MutatorMutex
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.withFrameNanos
+import com.airbnb.lottie.LottieComposition
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.NonCancellable
+import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.job
+import kotlinx.coroutines.withContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.coroutines.coroutineContext
+
+/**
+ * Use this to create a [LottieAnimatable] in a composable.
+ *
+ * @see LottieAnimatable
+ */
+@Composable
+fun rememberLottieAnimatable(): LottieAnimatable = remember { LottieAnimatable() }
+
+/**
+ * Use this to create a [LottieAnimatable] outside of a composable such as a hoisted state class.
+ *
+ * @see rememberLottieAnimatable
+ * @see LottieAnimatable
+ */
+fun LottieAnimatable(): LottieAnimatable = LottieAnimatableImpl()
+
+/**
+ * Reset the animation back to the minimum progress and first iteration.
+ */
+suspend fun LottieAnimatable.resetToBeginning() {
+    snapTo(
+        progress = defaultProgress(composition, clipSpec, speed),
+        iteration = 1,
+    )
+}
+
+/**
+ * [LottieAnimatable] is an extension of [LottieAnimationState] that contains imperative
+ * suspend functions to control animation playback.
+ *
+ * To create one, call:
+ * ```
+ * val animatable = rememberLottieAnimatable()
+ * ```
+ *
+ * This is the imperative version of [animateLottieCompositionAsState].
+ *
+ * [LottieAnimationState] ensures *mutual exclusiveness* on its animations. To
+ * achieve this, when a new animation is started via [animate] or [snapTo], any ongoing
+ * animation will be canceled via a [CancellationException]. Because of this, it is possible
+ * that your animation will not start synchronously. As a result, if you switch from animating
+ * one composition to another, it is not safe to render the second composition immediately after
+ * calling animate. Instead, you should always rely on [LottieAnimationState.composition] and
+ * [LottieAnimationState.progress].
+ *
+ * This class is comparable to [androidx.compose.animation.core.Animatable]. It is a relatively
+ * low-level API that gives maximum control over animations. In most cases, you can use
+ * [animateLottieCompositionAsState] which provides declarative APIs to create, update, and animate
+ * a [LottieComposition].
+ *
+ * @see animate
+ * @see snapTo
+ * @see animateLottieCompositionAsState
+ */
+@Stable
+interface LottieAnimatable : LottieAnimationState {
+    /**
+     * Snap to a specific point in an animation. This can be used to update the progress
+     * or iteration count of an ongoing animation. It will cancel any ongoing animations
+     * on this state class. To update and then resume an animation, call [animate] again with
+     * continueFromPreviousAnimate set to true after calling [snapTo].
+     *
+     * @param composition The [LottieComposition] that should be rendered.
+     *                    Defaults to [LottieAnimatable.composition].
+     * @param progress The progress that should be set.
+     *                 Defaults to [LottieAnimatable.progress]
+     * @param iteration Updates the current iteration count. This can be used to "rewind" or
+     *                  "fast-forward" an ongoing animation to a past/future iteration count.
+     *                   Defaults to [LottieAnimatable.iteration]
+     * @param resetLastFrameNanos [rememberLottieAnimatable] keeps track of the frame time of the most
+     *                            recent animation. When [animate] is called with continueFromPreviousAnimate
+     *                            set to true, a delta will be calculated from the most recent [animate] call
+     *                            to ensure that the original progress is unaffected by [snapTo] calls in the
+     *                            middle.
+     *                            Defaults to false if progress is not being snapped to.
+     *                            Defaults to true if progress is being snapped to.
+     */
+    suspend fun snapTo(
+        composition: LottieComposition? = this.composition,
+        progress: Float = this.progress,
+        iteration: Int = this.iteration,
+        resetLastFrameNanos: Boolean = progress != this.progress,
+    )
+
+    /**
+     * Animate a [LottieComposition].
+     *
+     * @param composition The [LottieComposition] that should be rendered.
+     * @param iteration The iteration to start the animation at. Defaults to 1 and carries over from previous animates.
+     * @param iterations The number of iterations to continue running for. Set to 1 to play one time
+     *                   set to [LottieConstants.IterateForever] to iterate forever. Can be set to arbitrary
+     *                   numbers. Defaults to 1 and carries over from previous animates.
+     * @param speed The speed at which the composition should be animated. Can be negative. Defaults to 1 and
+     *              carries over from previous animates.
+     * @param clipSpec An optional [LottieClipSpec] to trim the playback of the composition between two values.
+     *                 Defaults to null and carries over from previous animates.
+     * @param initialProgress An optional progress value that the animation should start at. Defaults to the
+     *                        starting progress as defined by the clipSpec and speed. Because the default value
+     *                        isn't the existing progress value, if you are resuming an animation, you
+     *                        probably want to set this to [progress].
+     * @param continueFromPreviousAnimate When set to true, instead of starting at the minimum progress,
+     *                                    the initial progress will be advanced in accordance to the amount
+     *                                    of time that has passed since the last frame was rendered.
+     * @param cancellationBehavior The behavior that this animation should have when cancelled. In most cases,
+     *                             you will want it to cancel immediately. However, if you have a state based
+     *                             transition and you want an animation to finish playing before moving on to
+     *                             the next one then you may want to set this to [LottieCancellationBehavior.OnIterationFinish].
+     */
+    suspend fun animate(
+        composition: LottieComposition?,
+        iteration: Int = this.iteration,
+        iterations: Int = this.iterations,
+        speed: Float = this.speed,
+        clipSpec: LottieClipSpec? = this.clipSpec,
+        initialProgress: Float =  defaultProgress(composition, clipSpec, speed),
+        continueFromPreviousAnimate: Boolean = false,
+        cancellationBehavior: LottieCancellationBehavior = LottieCancellationBehavior.Immediately,
+    )
+}
+
+@Stable
+private class LottieAnimatableImpl : LottieAnimatable {
+    override var isPlaying: Boolean by mutableStateOf(false)
+        private set
+
+    override var progress: Float by mutableStateOf(0f)
+        private set
+
+    override val value: Float
+        get() = progress
+
+    override var iteration: Int by mutableStateOf(1)
+        private set
+
+    override var iterations: Int by mutableStateOf(1)
+        private set
+
+    override var clipSpec: LottieClipSpec? by mutableStateOf(null)
+        private set
+
+    override var speed: Float by mutableStateOf(1f)
+        private set
+
+    override var composition: LottieComposition? by mutableStateOf(null)
+        private set
+
+    override var lastFrameNanos: Long by mutableStateOf(AnimationConstants.UnspecifiedTime)
+        private set
+
+    private val endProgress: Float by derivedStateOf {
+        val c = composition
+        when {
+            c == null -> 0f
+            speed < 0 -> clipSpec?.getMinProgress(c) ?: 0f
+            else -> clipSpec?.getMaxProgress(c) ?: 1f
+        }
+    }
+
+    override val isAtEnd: Boolean by derivedStateOf { iteration == iterations && progress == endProgress }
+
+    private val mutex = MutatorMutex()
+
+    override suspend fun snapTo(
+        composition: LottieComposition?,
+        progress: Float,
+        iteration: Int,
+        resetLastFrameNanos: Boolean,
+    ) {
+        mutex.mutate {
+            this.composition = composition
+            this.progress = progress
+            this.iteration = iteration
+            isPlaying = false
+            if (resetLastFrameNanos) {
+                lastFrameNanos = AnimationConstants.UnspecifiedTime
+            }
+        }
+    }
+
+    override suspend fun animate(
+        composition: LottieComposition?,
+        iteration: Int,
+        iterations: Int,
+        speed: Float,
+        clipSpec: LottieClipSpec?,
+        initialProgress: Float,
+        continueFromPreviousAnimate: Boolean,
+        cancellationBehavior: LottieCancellationBehavior,
+    ) {
+        mutex.mutate {
+            require(speed.isFinite()) { "Speed must be a finite number. It is $speed." }
+            this.iteration = iteration
+            this.iterations = iterations
+            this.speed = speed
+            this.clipSpec = clipSpec
+            this.composition = composition
+            this.progress = initialProgress
+            if (!continueFromPreviousAnimate) lastFrameNanos = AnimationConstants.UnspecifiedTime
+            if (composition == null) {
+                isPlaying = false
+                return@mutate
+            }
+
+            isPlaying = true
+            try {
+                val context = when (cancellationBehavior) {
+                    LottieCancellationBehavior.OnIterationFinish -> NonCancellable
+                    LottieCancellationBehavior.Immediately -> EmptyCoroutineContext
+                }
+                val parentJob = coroutineContext.job
+                withContext(context) {
+                    while (true) {
+                        val actualIterations = when (cancellationBehavior) {
+                            LottieCancellationBehavior.OnIterationFinish -> {
+                                if (parentJob.isActive) iterations else iteration
+                            }
+                            else -> iterations
+                        }
+                        if (!doFrame(actualIterations)) break
+                    }
+                }
+                coroutineContext.ensureActive()
+            } finally {
+                isPlaying = false
+            }
+        }
+    }
+
+    private suspend fun doFrame(iterations: Int): Boolean = withFrameNanos { frameNanos ->
+        val composition = composition ?: return@withFrameNanos true
+        val dNanos = if (lastFrameNanos == AnimationConstants.UnspecifiedTime) 0L else (frameNanos - lastFrameNanos)
+        lastFrameNanos = frameNanos
+
+        val minProgress = clipSpec?.getMinProgress(composition) ?: 0f
+        val maxProgress = clipSpec?.getMaxProgress(composition) ?: 1f
+
+        val dProgress = dNanos / 1_000_000 / composition.duration * speed
+        val progressPastEndOfIteration = when {
+            speed < 0 -> minProgress - (progress + dProgress)
+            else -> progress + dProgress - maxProgress
+        }
+        if (progressPastEndOfIteration < 0f) {
+            progress += dProgress
+        } else {
+            val durationProgress = maxProgress - minProgress
+            val dIterations = (progressPastEndOfIteration / durationProgress).toInt() + 1
+
+            if (iteration + dIterations > iterations) {
+                progress = endProgress
+                iteration = iterations
+                return@withFrameNanos false
+            }
+            iteration += dIterations
+            val progressPastEndRem = progressPastEndOfIteration - (dIterations - 1) * durationProgress
+            progress = when {
+                speed < 0 -> maxProgress - progressPastEndRem
+                else -> minProgress + progressPastEndRem
+            }
+        }
+
+        true
+    }
+}
+
+private fun defaultProgress(composition: LottieComposition?, clipSpec: LottieClipSpec?, speed: Float): Float {
+    return when {
+        speed < 0 && composition == null -> 1f
+        composition == null -> 0f
+        speed < 0 -> clipSpec?.getMaxProgress(composition) ?: 1f
+        else -> clipSpec?.getMinProgress(composition) ?: 0f
+    }
+}
\ No newline at end of file
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimation.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimation.kt
index 199f6af..4a8a6c9 100644
--- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimation.kt
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimation.kt
@@ -2,150 +2,164 @@
 
 import androidx.annotation.FloatRange
 import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.runtime.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
 import androidx.compose.ui.graphics.drawscope.withTransform
 import androidx.compose.ui.graphics.nativeCanvas
 import androidx.compose.ui.platform.LocalContext
+import com.airbnb.lottie.ImageAssetDelegate
 import com.airbnb.lottie.LottieComposition
-import com.airbnb.lottie.LottieCompositionFactory
 import com.airbnb.lottie.LottieDrawable
 import com.airbnb.lottie.manager.ImageAssetManager
 import com.airbnb.lottie.setImageAssetManager
-import java.io.FileInputStream
-import java.util.concurrent.TimeUnit
-import java.util.zip.ZipInputStream
-import kotlin.math.floor
-
 
 /**
- * TODO: add error handling
+ * This is the base LottieAnimation composable. It takes a composition and renders it at a specific progress.
+ *
+ * The overloaded version of [LottieAnimation] that handles playback and is sufficient for most use cases.
+ *
+ * @param composition The composition that will be rendered. To generate a [LottieComposition], you can use
+ *                    [rememberLottieComposition].
+ * @param progress The progress (between 0 and 1) that should be rendered. If you want to render a specific
+ *                 frame, you can use [LottieComposition.getFrameForProgress]. In most cases, you will want
+ *                 to use one of th overloaded LottieAnimation composables that drives the animation for you.
+ *                 The overloads that have isPlaying as a parameter instead of progress will drive the
+ *                 animation automatically. You may want to use this version if you want to drive the animation
+ *                 from your own Animatable or via events such as download progress or a gesture.
+ * @param imageAssetsFolder If you use image assets, you must explicitly specify the folder in assets/ in which
+ *                          they are located because bodymovin uses the name filenames across all
+ *                          compositions (img_#). Do NOT rename the images themselves.
+ *                          If your images are located in src/main/assets/airbnb_loader/ then imageAssetsFolder
+ *                          should be set to "airbnb_loader"
+ *                          Be wary if you are using many images, however. Lottie is designed to work with
+ *                          vector shapes from After Effects. If your images look like they could be
+ *                          represented with vector shapes, see if it is possible to convert them to shape
+ *                          layers and re-export your animation. Check the documentation at
+ *                          http://airbnb.io/lottie for more information about importing shapes from Sketch
+ *                          or Illustrator to avoid this.
+ * @param imageAssetDelegate Use this if you can't bundle images with your app. This may be useful if you
+ *                           download the animations from the network or have the images saved to an SD Card.
+ *                           In that case, Lottie will defer the loading of the bitmap to this delegate.
+ *                           Be wary if you are using many images, however. Lottie is designed to work with
+ *                           vector shapes from After Effects. If your images look like they could be
+ *                           represented with vector shapes, see if it is possible to convert them to shape
+ *                           layers and re-export your animation. Check the documentation at
+ *                           http://airbnb.io/lottie for more information about importing shapes from Sketch
+ *                           or Illustrator to avoid this.
+ * @param outlineMasksAndMattes Enable this to debug slow animations by outlining masks and mattes.
+ *                              The performance overhead of the masks and mattes will be proportional to the
+ *                              surface area of all of the masks/mattes combined.
+ *                              DO NOT leave this enabled in production.
+ * @param applyOpacityToLayers Sets whether to apply opacity to the each layer instead of shape.
+ *                             Opacity is normally applied directly to a shape. In cases where translucent
+ *                             shapes overlap, applying opacity to a layer will be more accurate at the
+ *                             expense of performance.
+ *                             Note: This process is very expensive. The performance impact will be reduced
+ *                             when hardware acceleration is enabled.
+ * @param enableMergePaths Enables experimental merge paths support. Most animations with merge paths will
+ *                         want this on but merge path support is more limited than some other rendering
+ *                         features so it defaults to off. The only way to know if your animation will work
+ *                         well with merge paths or not is to try it. If your animation has merge paths and
+ *                         doesn't render correctly, please file an issue.
  */
 @Composable
-fun rememberLottieComposition(spec: LottieAnimationSpec): LottieCompositionResult {
-    val context = LocalContext.current
-    var result: LottieCompositionResult by remember { mutableStateOf(LottieCompositionResult.Loading) }
-    DisposableEffect(spec) {
-        var isDisposed = false
-        val task = when (spec) {
-            is LottieAnimationSpec.RawRes -> LottieCompositionFactory.fromRawRes(context, spec.resId)
-            is LottieAnimationSpec.Url -> LottieCompositionFactory.fromUrl(context, spec.url)
-            is LottieAnimationSpec.File -> {
-                val fis = FileInputStream(spec.fileName)
-                when {
-                    spec.fileName.endsWith("zip") -> LottieCompositionFactory.fromZipStream(ZipInputStream(fis), spec.fileName)
-                    else -> LottieCompositionFactory.fromJsonInputStream(fis, spec.fileName)
-                }
-            }
-            is LottieAnimationSpec.Asset -> LottieCompositionFactory.fromAsset(context, spec.assetName)
-        }
-        task.addListener { c ->
-            if (!isDisposed) result = LottieCompositionResult.Success(c)
-        }.addFailureListener { e ->
-            if (!isDisposed) {
-                result = LottieCompositionResult.Fail(e)
-            }
-        }
-        onDispose {
-            isDisposed = true
-        }
-    }
-    return result
-}
-
-@Composable
-fun LottieAnimation(
-    spec: LottieAnimationSpec,
-    modifier: Modifier = Modifier,
-    animationState: LottieAnimationState = rememberLottieAnimationState(autoPlay = true),
-) {
-    val composition = rememberLottieComposition(spec)
-    LottieAnimation(composition, modifier, animationState)
-}
-
-@Composable
-fun LottieAnimation(
-    compositionResult: LottieCompositionResult,
-    modifier: Modifier = Modifier,
-    animationState: LottieAnimationState = rememberLottieAnimationState(autoPlay = true),
-) {
-    LottieAnimation(compositionResult(), animationState, modifier)
-}
-
-@Composable
 fun LottieAnimation(
     composition: LottieComposition?,
-    state: LottieAnimationState,
+    @FloatRange(from = 0.0, to = 1.0) progress: Float,
     modifier: Modifier = Modifier,
+    imageAssetsFolder: String? = null,
+    imageAssetDelegate: ImageAssetDelegate? = null,
+    outlineMasksAndMattes: Boolean = false,
+    applyOpacityToLayers: Boolean = false,
+    enableMergePaths: Boolean = false,
 ) {
     val drawable = remember { LottieDrawable() }
-    var imageAssetManager: ImageAssetManager? by remember { mutableStateOf(null) }
+    var imageAssetManager by remember { mutableStateOf<ImageAssetManager?>(null) }
 
-    if (composition?.hasImages() == true) {
+    if (composition == null || composition.duration == 0f) return Box(modifier)
+
+    if (composition.hasImages()) {
         val context = LocalContext.current
-        LaunchedEffect(context, composition, state.imageAssetsFolder, state.imageAssetDelegate) {
-            @Suppress("RestrictedApi")
-            imageAssetManager = ImageAssetManager(context, state.imageAssetsFolder, state.imageAssetDelegate, composition.images)
+        LaunchedEffect(context, composition, imageAssetsFolder, imageAssetDelegate) {
+            imageAssetManager = ImageAssetManager(context, imageAssetsFolder, imageAssetDelegate, composition.images)
         }
     } else {
         imageAssetManager = null
     }
 
-    SideEffect {
-        drawable.composition = composition
-    }
-
-    // TODO: handle min/max frame setting
-
-    LaunchedEffect(composition, state.isPlaying) {
-        if (!state.isPlaying || composition == null) return@LaunchedEffect
-        var repeatCount = 0
-        if (state.isPlaying && state.progress == 1f) state.progress = 0f
-        var lastFrameTime = withFrameNanos { it }
-        while (true) {
-            withFrameNanos { frameTime ->
-                val dTime = (frameTime - lastFrameTime) / TimeUnit.MILLISECONDS.toNanos(1).toFloat()
-                lastFrameTime = frameTime
-                val dProgress = (dTime * state.speed) / composition.duration
-                val previousProgress = state.progress
-                state.progress = (state.progress + dProgress) % 1f
-                if (previousProgress > state.progress) {
-                    repeatCount++
-                    if (repeatCount != 0 && repeatCount > state.repeatCount) {
-                        state.progress = 1f
-                        state.isPlaying = false
-                    }
-                }
-                val frame = floor(lerp(drawable.minFrame, drawable.maxFrame, state.progress)).toInt()
-                state.updateFrame(frame)
-            }
-        }
-    }
-
-    if (composition == null || composition.duration == 0f) return
-
     Canvas(
         modifier = modifier
             .maintainAspectRatio(composition)
     ) {
         drawIntoCanvas { canvas ->
-            state.applyTo(drawable)
-            drawable.setImageAssetManager(imageAssetManager)
             withTransform({
                 scale(size.width / composition.bounds.width().toFloat(), size.height / composition.bounds.height().toFloat(), Offset.Zero)
             }) {
+                drawable.composition = composition
+                drawable.setOutlineMasksAndMattes(outlineMasksAndMattes)
+                drawable.isApplyingOpacityToLayersEnabled = applyOpacityToLayers
+                drawable.enableMergePathsForKitKatAndAbove(enableMergePaths)
+                drawable.setImageAssetManager(imageAssetManager)
+                drawable.progress = progress
                 drawable.draw(canvas.nativeCanvas)
             }
         }
     }
 }
 
-private fun Modifier.maintainAspectRatio(composition: LottieComposition?): Modifier {
-    composition ?: return this
-    return this.then(aspectRatio(composition.bounds.width() / composition.bounds.height().toFloat()))
+/**
+ * This is like [LottieAnimation] except that it handles driving the animation via [animateLottieCompositionAsState]
+ * instead of taking a raw progress parameter.
+ *
+ * @see LottieAnimation
+ * @see animateLottieCompositionAsState
+ */
+@Composable
+fun LottieAnimation(
+    composition: LottieComposition?,
+    modifier: Modifier = Modifier,
+    isPlaying: Boolean = true,
+    restartOnPlay: Boolean = true,
+    clipSpec: LottieClipSpec? = null,
+    speed: Float = 1f,
+    iterations: Int = 1,
+    imageAssetsFolder: String? = null,
+    imageAssetDelegate: ImageAssetDelegate? = null,
+    outlineMasksAndMattes: Boolean = false,
+    applyOpacityToLayers: Boolean = false,
+    enableMergePaths: Boolean = false,
+) {
+    val progress by animateLottieCompositionAsState(
+        composition,
+        isPlaying,
+        restartOnPlay,
+        clipSpec,
+        speed,
+        iterations,
+    )
+    LottieAnimation(
+        composition,
+        progress,
+        modifier,
+        imageAssetsFolder,
+        imageAssetDelegate,
+        outlineMasksAndMattes,
+        applyOpacityToLayers,
+        enableMergePaths,
+    )
 }
 
-private fun lerp(a: Float, b: Float, @FloatRange(from = 0.0, to = 1.0) percentage: Float) = a + percentage * (b - a)
+private fun Modifier.maintainAspectRatio(composition: LottieComposition?): Modifier {
+    composition ?: return this
+    // TODO: use ContentScale and a transform here
+    return this.then(aspectRatio(composition.bounds.width() / composition.bounds.height().toFloat()))
+}
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimationSpec.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimationSpec.kt
deleted file mode 100644
index 47393bb..0000000
--- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimationSpec.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.airbnb.lottie.compose
-
-/**
- * Specification for a Lottie animation. Each subclass represents a different source.
- */
-sealed class LottieAnimationSpec {
-    data class RawRes(@androidx.annotation.RawRes val resId: Int) : LottieAnimationSpec()
-    data class Url(val url: String) : LottieAnimationSpec()
-    data class File(val fileName: String) : LottieAnimationSpec()
-    data class Asset(val assetName: String) : LottieAnimationSpec()
-}
\ No newline at end of file
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimationState.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimationState.kt
index 6801bd0..0cbf1ac 100644
--- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimationState.kt
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimationState.kt
@@ -1,155 +1,37 @@
 package com.airbnb.lottie.compose
 
-import androidx.compose.runtime.*
-import com.airbnb.lottie.ImageAssetDelegate
-import com.airbnb.lottie.LottieDrawable
+import androidx.compose.animation.core.AnimationConstants
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
+import com.airbnb.lottie.LottieComposition
 
 /**
- * Create a [LottieAnimationState] and remember it
+ * [LottieAnimationState] is a value holder that contains information about the current Lottie animation.
  *
- * @param autoPlay Initial value for [LottieAnimationState.isPlaying]
- * @param repeatCount Initial value for [LottieAnimationState.repeatCount]
- * @param initialProgress Initial value for [LottieAnimationState.progress]
- * @param enableMergePaths Initial value for [LottieAnimationState.enableMergePaths]
- * @param imageAssetsFolder Initial value for [LottieAnimationState.imageAssetsFolder]
- * @param imageAssetDelegate Initial value for [LottieAnimationState.imageAssetDelegate]
+ * The primary values are [LottieAnimationState.progress] and [LottieAnimationState.composition]. These
+ * value should be passed into the main [LottieAnimation] composable.
+ *
+ * @see progress
+ * @see composition
+ * @see animateLottieCompositionAsState
  */
-@Composable
-fun rememberLottieAnimationState(
-    autoPlay: Boolean = true,
-    repeatCount: Int = 0,
-    initialProgress: Float = 0f,
-    enableMergePaths: Boolean = true,
-    imageAssetsFolder: String? = null,
-    imageAssetDelegate: ImageAssetDelegate? = null,
+@Stable
+interface LottieAnimationState : State<Float> {
+    val isPlaying: Boolean
 
-): LottieAnimationState {
-    // Use rememberSavedInstanceState so you can pause/resume animations
-    return remember(repeatCount, autoPlay) {
-        LottieAnimationState(
-            isPlaying = autoPlay,
-            repeatCount = repeatCount,
-            initialProgress = initialProgress,
-            enableMergePaths = enableMergePaths,
-            imageAssetsFolder = imageAssetsFolder,
-            imageAssetDelegate = imageAssetDelegate,
-        )
-    }
-}
+    val progress: Float
 
-/**
- * State of the [LottieAnimation] composable
- *
- * @param isPlaying Initial value for [isPlaying]
- * @param repeatCount Initial value for [repeatCount]
- * @param initialProgress Initial value for [progress]
- * @param enableMergePaths Initial value for [enableMergePaths]
- * @param imageAssetsFolder Initial value for [LottieAnimationState.imageAssetsFolder]
- * @param imageAssetDelegate Initial value for [LottieAnimationState.imageAssetDelegate]
- *
- * @see rememberLottieAnimationState
- */
-class LottieAnimationState(
-    isPlaying: Boolean,
-    repeatCount: Int = 0,
-    initialProgress: Float = 0f,
-    enableMergePaths: Boolean = true,
-    imageAssetsFolder: String? = null,
-    imageAssetDelegate: ImageAssetDelegate? = null,
-) {
-    var progress by mutableStateOf(initialProgress)
+    val iteration: Int
 
-    // TODO: make this public
-    private var _frame = mutableStateOf(0)
-    val frame: Int by _frame
+    val iterations: Int
 
-    /**
-     * Whether the animation is currently playing.
-     */
-    var isPlaying by mutableStateOf(isPlaying)
+    val clipSpec: LottieClipSpec?
 
-    /**
-     * How many times the animation will be played. Use [Int.MAX_VALUE] for
-     * infinite repetitions.
-     */
-    var repeatCount by mutableStateOf(repeatCount)
+    val speed: Float
 
-    var speed by mutableStateOf(1f)
+    val composition: LottieComposition?
 
-    /**
-     * Enable this to debug slow animations by outlining masks and mattes. The performance overhead of the masks and mattes will
-     * be proportional to the surface area of all of the masks/mattes combined.
-     * <p>
-     * DO NOT leave this enabled in production.
-     */
-    var outlineMasksAndMattes by mutableStateOf(false)
+    val lastFrameNanos: Long get() = AnimationConstants.UnspecifiedTime
 
-
-    /**
-     * Sets whether to apply opacity to the each layer instead of shape.
-     * <p>
-     * Opacity is normally applied directly to a shape. In cases where translucent shapes overlap, applying opacity to a layer will be more accurate
-     * at the expense of performance.
-     * <p>
-     * The default value is false.
-     * <p>
-     * Note: This process is very expensive and will incur additional performance overhead.
-     */
-    var applyOpacityToLayers by mutableStateOf(false)
-
-    /**
-     * Enable this to get merge path support.
-     * <p>
-     * Merge paths currently don't work if the the operand shape is entirely contained within the
-     * first shape. If you need to cut out one shape from another shape, use an even-odd fill type
-     * instead of using merge paths.
-     * <p>
-     * If your animation contains merge paths and you are encountering rendering issues, disabling
-     * merge paths might help.
-     */
-    var enableMergePaths by mutableStateOf(enableMergePaths)
-
-    /**
-     * If you use image assets, you must explicitly specify the folder in assets/ in which they are
-     * located because bodymovin uses the name filenames across all compositions (img_#).
-     * Do NOT rename the images themselves.
-     * <p>
-     * If your images are located in src/main/assets/airbnb_loader/ then set this to "airbnb_loader".
-     * <p>
-     * <p>
-     * Be wary if you are using many images, however. Lottie is designed to work with vector shapes
-     * from After Effects. If your images look like they could be represented with vector shapes,
-     * see if it is possible to convert them to shape layers and re-export your animation. Check
-     * the documentation at https://airbnb.io/lottie for more information about importing shapes from
-     * Sketch or Illustrator to avoid this.
-     */
-    var imageAssetsFolder by mutableStateOf(imageAssetsFolder)
-
-    /**
-     * Use this if you can't bundle images with your app. This may be useful if you download the
-     * animations from the network or have the images saved to an SD Card. In that case, Lottie
-     * will defer the loading of the bitmap to this delegate.
-     * <p>
-     * Be wary if you are using many images, however. Lottie is designed to work with vector shapes
-     * from After Effects. If your images look like they could be represented with vector shapes,
-     * see if it is possible to convert them to shape layers and re-export your animation. Check
-     * the documentation at https://airbnb.io/lottie for more information about importing shapes from
-     * Sketch or Illustrator to avoid this.
-     */
-    var imageAssetDelegate by mutableStateOf(imageAssetDelegate)
-
-    internal fun updateFrame(frame: Int) {
-        _frame.value = frame
-    }
-
-    fun toggleIsPlaying() {
-        isPlaying = !isPlaying
-    }
-
-    internal fun applyTo(drawable: LottieDrawable) {
-        drawable.progress = progress
-        drawable.setOutlineMasksAndMattes(outlineMasksAndMattes)
-        drawable.isApplyingOpacityToLayersEnabled = applyOpacityToLayers
-        drawable.enableMergePathsForKitKatAndAbove(enableMergePaths)
-    }
+    val isAtEnd: Boolean
 }
\ No newline at end of file
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieCancellationBehavior.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieCancellationBehavior.kt
new file mode 100644
index 0000000..2126705
--- /dev/null
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieCancellationBehavior.kt
@@ -0,0 +1,20 @@
+package com.airbnb.lottie.compose
+
+/**
+ * Determines how the animation should behave if the calling CoroutineScope is cancelled.
+ *
+ * @see rememberLottieAnimatable
+ */
+enum class LottieCancellationBehavior {
+    /**
+     * Stop animation immediately and return early.
+     */
+    Immediately,
+
+    /**
+     * Delay cancellations until the current iteration has fully completed.
+     * This can be useful in state based transitions where you want one animation to finish its
+     * animation before continuing to the next.
+     */
+    OnIterationFinish,
+}
\ No newline at end of file
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieClipSpec.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieClipSpec.kt
new file mode 100644
index 0000000..8c1b335
--- /dev/null
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieClipSpec.kt
@@ -0,0 +1,107 @@
+package com.airbnb.lottie.compose
+
+import com.airbnb.lottie.LottieComposition
+
+/**
+ * Use subclasses of [LottieClipSpec] to set min/max bounds on the animation playback.
+ *
+ * @see LottieAnimation
+ * @see rememberLottieAnimatable
+ * @see animateLottieCompositionAsState
+ */
+sealed class LottieClipSpec {
+
+    internal abstract fun getMinProgress(composition: LottieComposition): Float
+
+    internal abstract fun getMaxProgress(composition: LottieComposition): Float
+
+    /**
+     * Play the animation between these two frames. [maxInclusive] determines whether the animation
+     * should play the max frame or stop one frame before it.
+     */
+    data class Frame(
+        val min: Int? = null,
+        val max: Int? = null,
+        val maxInclusive: Boolean = true,
+    ) : LottieClipSpec() {
+
+        private val actualMaxFrame = when {
+            max == null -> null
+            maxInclusive -> max
+            else -> max - 1
+        }
+
+        override fun getMinProgress(composition: LottieComposition): Float {
+            return when (min) {
+                null -> 0f
+                else -> (min / composition.endFrame).coerceIn(0f, 1f)
+            }
+        }
+
+        override fun getMaxProgress(composition: LottieComposition): Float {
+            return when (actualMaxFrame) {
+                null -> 1f
+                else -> (actualMaxFrame / composition.endFrame).coerceIn(0f, 1f)
+            }
+        }
+    }
+
+    /**
+     * Play the animation between these two progress values.
+     */
+    data class Progress(
+        val min: Float = 0f,
+        val max: Float = 1f,
+    ) : LottieClipSpec() {
+        override fun getMinProgress(composition: LottieComposition): Float {
+            return min
+        }
+
+        override fun getMaxProgress(composition: LottieComposition): Float {
+            return max
+        }
+    }
+
+    /**
+     * Play the animation from minMarker until maxMarker. If maxMarker represents the end of your animation,
+     * set [maxInclusive] to true. If the marker represents the beginning of the next section, set
+     * it to false to stop the animation at the frame before maxMarker.
+     */
+    data class Markers(
+        val min: String? = null,
+        val max: String? = null,
+        val maxInclusive: Boolean = true
+    ) : LottieClipSpec() {
+        override fun getMinProgress(composition: LottieComposition): Float {
+            return when (min) {
+                null -> 0f
+                else -> ((composition.getMarker(min)?.startFrame ?: 0f) / composition.endFrame).coerceIn(0f, 1f)
+            }
+        }
+
+        override fun getMaxProgress(composition: LottieComposition): Float {
+            return when (max) {
+                null -> 1f
+                else -> {
+                    val offset = if (maxInclusive) 0 else -1
+                    return ((composition.getMarker(max)?.startFrame?.plus(offset) ?: 0f) / composition.endFrame).coerceIn(0f, 1f)
+                }
+            }
+        }
+    }
+
+    /**
+     * Play the animation from the beginning of the marker for the duration of the marker itself.
+     * The duration can be set in After Effects.
+     */
+    data class Marker(val marker: String) : LottieClipSpec() {
+        override fun getMinProgress(composition: LottieComposition): Float {
+            return ((composition.getMarker(marker)?.startFrame ?: 0f) / composition.endFrame).coerceIn(0f, 1f)
+        }
+
+        override fun getMaxProgress(composition: LottieComposition): Float {
+            val marker = composition.getMarker(marker) ?: return 1f
+            return ((marker.startFrame + marker.durationFrames) / composition.endFrame).coerceIn(0f, 1f)
+        }
+    }
+}
\ No newline at end of file
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieCompositionResult.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieCompositionResult.kt
index 8a031d6..6b5e37b 100644
--- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieCompositionResult.kt
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieCompositionResult.kt
@@ -1,17 +1,128 @@
 package com.airbnb.lottie.compose
 
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
 import com.airbnb.lottie.LottieComposition
+import kotlinx.coroutines.CompletableDeferred
 
-sealed class LottieCompositionResult {
-    open val composition: LottieComposition? = null
+/**
+ * A [LottieCompositionResult] subclass is returned from [rememberLottieComposition].
+ * It can be completed with a result or exception only one time.
+ *
+ * This class implements State<LottieComposition> so you either use it like:
+ * ```
+ * val compositionResult = rememberLottieComposition(...)
+ * // Or
+ * val composition by rememberLottieComposition(...)
+ * ```
+ *
+ * Use the former if you need to explicitly differentiate between loading and error states
+ * or if you need to call [await] or [awaitOrNull] in a coroutine such as [androidx.compose.runtime.LaunchedEffect].
+ *
+ * @see rememberLottieComposition
+ * @see LottieAnimation
+ */
+@Stable
+interface LottieCompositionResult : State<LottieComposition?> {
+    /**
+     * The composition or null if it hasn't yet loaded or failed to load.
+     */
+    override val value: LottieComposition?
 
-    open operator fun invoke(): LottieComposition? = composition
+    /**
+     * The exception that was thrown while trying to load and parse the composition.
+     */
+    val error: Throwable?
 
-    object Loading : LottieCompositionResult()
+    /**
+     * Whether or not the composition is still being loaded and parsed.
+     */
+    val isLoading: Boolean
 
-    data class Success(override val composition: LottieComposition) : LottieCompositionResult() {
-        override operator fun invoke(): LottieComposition = composition
+    /**
+     * Whether or not the composition is in the process of being loaded or parsed.
+     */
+    val isComplete: Boolean
+
+    /**
+     * Whether or not the composition failed to load. This is terminal. It only occurs after
+     * returning false from [rememberLottieComposition]'s onRetry lambda.
+     */
+    val isFailure: Boolean
+
+    /**
+     * Whether or not the composition has succeeded yet.
+     */
+    val isSuccess: Boolean
+
+    /**
+     * Suspend until the composition has finished parsing.
+     *
+     * This can throw if the [LottieComposition] fails to load.
+     *
+     * These animations should never fail given a valid input:
+     * * [LottieCompositionSpec.RawRes]
+     * * [LottieCompositionSpec.Asset]
+     * * [LottieCompositionSpec.JsonString]
+     *
+     * These animations may fail:
+     * * [LottieCompositionSpec.Url]
+     * * [LottieCompositionSpec.File]
+     */
+    suspend fun await(): LottieComposition
+}
+
+/**
+ * Like [LottieCompositionResult.await] but returns null instead of throwing an exception if the animation fails
+ * to load.
+ */
+suspend fun LottieCompositionResult.awaitOrNull(): LottieComposition? {
+    return try {
+        await()
+    } catch (e: Throwable) {
+        null
+    }
+}
+
+@Stable
+internal class LottieCompositionResultImpl(): LottieCompositionResult {
+    private val compositionDeferred = CompletableDeferred<LottieComposition>()
+
+    override var value: LottieComposition? by mutableStateOf(null)
+        private set
+
+    override var error by mutableStateOf<Throwable?>(null)
+        private set
+
+    override val isLoading by derivedStateOf { value == null && error == null }
+
+    override val isComplete by derivedStateOf { value != null || error != null }
+
+    override val isFailure by derivedStateOf { error != null }
+
+    override val isSuccess by derivedStateOf { value != null }
+
+    override suspend fun await(): LottieComposition {
+        return compositionDeferred.await()
     }
 
-    data class Fail(val e: Throwable) : LottieCompositionResult()
+    @Synchronized
+    internal fun complete(composition: LottieComposition) {
+        if (isComplete) return
+
+        this.value = composition
+        compositionDeferred.complete(composition)
+    }
+
+    @Synchronized
+    internal fun completeExceptionally(error: Throwable) {
+        if (isComplete) return
+
+        this.error = error
+        compositionDeferred.completeExceptionally(error)
+    }
 }
\ No newline at end of file
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieCompositionSpec.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieCompositionSpec.kt
new file mode 100644
index 0000000..69877fb
--- /dev/null
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieCompositionSpec.kt
@@ -0,0 +1,43 @@
+package com.airbnb.lottie.compose
+
+/**
+ * Specification for a [com.airbnb.lottie.LottieComposition]. Each subclass represents a different source.
+ * A [com.airbnb.lottie.LottieComposition] is the stateless parsed version of a Lottie json file and is
+ * passed into [rememberLottieComposition] or [LottieAnimation].
+ */
+sealed class LottieCompositionSpec {
+    /**
+     * Load an animation from res/raw.
+     */
+    data class RawRes(@androidx.annotation.RawRes val resId: Int) : LottieCompositionSpec()
+
+    /**
+     * Load an animation from the internet. Lottie has a default network stack that will use
+     * standard Android networking APIs to attempt to load your animation. You may want to
+     * integrate your own networking stack instead for consistency, to add your own headers,
+     * or implement retries. To do that, call [com.airbnb.lottie.Lottie.initialize] and set
+     * a [com.airbnb.lottie.network.LottieNetworkFetcher] on the [com.airbnb.lottie.LottieConfig].
+     *
+     * If you are using this spec, you may want to use [rememberLottieComposition] instead of
+     * passing this spec directly into [LottieAnimation] because it can fail and you want to
+     * make sure that you properly handle the failures and/or retries.
+     */
+    data class Url(val url: String) : LottieCompositionSpec()
+
+    /**
+     * Load an animation from an arbitrary file. Make sure that your app has permissions to read it
+     * or else this may fail.
+     */
+    data class File(val fileName: String) : LottieCompositionSpec()
+
+    /**
+     * Load an animation from the assets directory of your app. This isn't type safe like [RawRes]
+     * so make sure that the path to your animation is correct this will fail.
+     */
+    data class Asset(val assetName: String) : LottieCompositionSpec()
+
+    /**
+     * Load an animation from its json string.
+     */
+    data class JsonString(val jsonString: String, val cacheKey: String? = null) : LottieCompositionSpec()
+}
\ No newline at end of file
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieConstants.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieConstants.kt
new file mode 100644
index 0000000..9ca32d9
--- /dev/null
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieConstants.kt
@@ -0,0 +1,8 @@
+package com.airbnb.lottie.compose
+
+object LottieConstants {
+    /**
+     * Use this with [animateLottieComposition#iterations] to repeat forever.
+     */
+    const val IterateForever = Integer.MAX_VALUE
+}
\ No newline at end of file
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieRetrySignal.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieRetrySignal.kt
new file mode 100644
index 0000000..4ccb67f
--- /dev/null
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieRetrySignal.kt
@@ -0,0 +1,50 @@
+package com.airbnb.lottie.compose
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.trySendBlocking
+
+/**
+ * @see LottieRetrySignal
+ * @see rememberLottieComposition
+ */
+@Composable
+fun rememberLottieRetrySignal(): LottieRetrySignal {
+    return remember { LottieRetrySignal() }
+}
+
+/**
+ * Helper to retry compositions that fail to load. This will mostly happen for animations loaded via url.
+ *
+ * Call [retry] from an action that should trigger a retry such as a snackbar or retry button.
+ *
+ * Call [awaitRetry] from the onRetry lambda to [rememberLottieComposition] then return true.
+ *
+ * @see rememberLottieComposition
+ */
+@Stable
+class LottieRetrySignal internal constructor() {
+    private val channel = Channel<Unit>(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
+
+    var isAwaitingRetry: Boolean by mutableStateOf(false)
+        private set
+
+    fun retry() {
+        channel.trySendBlocking(Unit)
+    }
+
+    suspend fun awaitRetry() {
+        try {
+            isAwaitingRetry = true
+            channel.receive()
+        } finally {
+            isAwaitingRetry = false
+        }
+    }
+}
\ No newline at end of file
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/animateLottieCompositionAsState.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/animateLottieCompositionAsState.kt
new file mode 100644
index 0000000..d6e5505
--- /dev/null
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/animateLottieCompositionAsState.kt
@@ -0,0 +1,70 @@
+package com.airbnb.lottie.compose
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import com.airbnb.lottie.LottieComposition
+
+/**
+ * Returns a [LottieAnimationState] representing the progress of an animation.
+ *
+ * This is the declarative version of [rememberLottieAnimatable] and [LottieAnimation].
+ *
+ * @param composition The composition to render. This should be retrieved with [rememberLottieComposition].
+ * @param isPlaying Whether or not the animation is currently playing. Note that the internal
+ *                  animation may end due to reaching the target iterations count. If that happens,
+ *                  the animation may stop even if this is still true. You can observe the returned
+ *                  [LottieAnimationState.isPlaying] to determine whether the underlying animation
+ *                  is still playing.
+ * @param restartOnPlay If isPlaying switches from false to true, restartOnPlay determines whether
+ *                      the progress and iteration gets reset.
+ * @param clipSpec A [LottieClipSpec] that specifies the bound the animation playback
+ *                 should be clipped to.
+ * @param speed The speed the animation should play at. Numbers larger than one will speed it up.
+ *              Numbers between 0 and 1 will slow it down. Numbers less than 0 will play it backwards.
+ * @param iterations The number of times the animation should repeat before stopping. It must be
+ *                    a positive number. [LottieConstants.IterateForever] can be used to repeat forever.
+ */
+@Composable
+fun animateLottieCompositionAsState(
+    composition: LottieComposition?,
+    isPlaying: Boolean = true,
+    restartOnPlay: Boolean = true,
+    clipSpec: LottieClipSpec? = null,
+    speed: Float = 1f,
+    iterations: Int = 1,
+): LottieAnimationState {
+    require(iterations > 0) { "Iterations must be a positive number ($iterations)." }
+    require(speed.isFinite()) { "Speed must be a finite number. It is $speed." }
+
+    val animatable = rememberLottieAnimatable()
+    var wasPlaying by remember { mutableStateOf(isPlaying) }
+
+    LaunchedEffect(
+        composition,
+        isPlaying,
+        clipSpec,
+        speed,
+        iterations,
+    ) {
+        if (isPlaying && !wasPlaying && restartOnPlay) {
+            animatable.resetToBeginning()
+        }
+        wasPlaying = isPlaying
+        if (!isPlaying) return@LaunchedEffect
+
+        animatable.animate(
+            composition,
+            iterations = iterations,
+            speed = speed,
+            clipSpec = clipSpec,
+            initialProgress = animatable.progress,
+            continueFromPreviousAnimate = false,
+        )
+    }
+
+    return animatable
+}
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/rememberLottieComposition.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/rememberLottieComposition.kt
new file mode 100644
index 0000000..49bf872
--- /dev/null
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/rememberLottieComposition.kt
@@ -0,0 +1,86 @@
+package com.airbnb.lottie.compose
+
+import android.content.Context
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import com.airbnb.lottie.LottieComposition
+import com.airbnb.lottie.LottieCompositionFactory
+import kotlinx.coroutines.suspendCancellableCoroutine
+import java.io.FileInputStream
+import java.util.zip.ZipInputStream
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+
+/**
+ * Takes a [LottieCompositionSpec], attempts to load and parse the animation, and returns a [LottieCompositionResult].
+ *
+ * [LottieCompositionResult] allows you to explicitly check for loading, failures, call
+ * [LottieCompositionResult.await], or invoke it like a function to get the nullable composition.
+ *
+ * [LottieCompositionResult] implements State<LottieComposition?> so if you don't need the full result class,
+ * you can use this function like:
+ * ```
+ * val compositionResult: LottieCompositionResult = lottieComposition(spec)
+ * // or...
+ * val composition: State<LottieComposition?> by lottieComposition(spec)
+ * ```
+ *
+ * @param spec The [LottieCompositionSpec] that defines which LottieComposition should be loaded.
+ * @param onRetry An optional callback that will be called if loading the animation fails.
+ *                It is passed the failed count (the number of times it has failed) and the exception
+ *                from the previous attempt to load the composition. [onRetry] is a suspending function
+ *                so you can do things like add a backoff delay or await an internet connection before
+ *                retrying again. [rememberLottieRetrySignal] can be used to handle explicit retires.
+ */
+@Composable
+fun rememberLottieComposition(
+    spec: LottieCompositionSpec,
+    onRetry: suspend (failCount: Int, previousException: Throwable) -> Boolean = { _, _ -> false },
+): LottieCompositionResult {
+    val context = LocalContext.current
+    val result by remember(spec) { mutableStateOf(LottieCompositionResultImpl()) }
+    LaunchedEffect(spec) {
+        var exception: Throwable? = null
+        var failedCount = 0
+        while (!result.isSuccess && (failedCount == 0 || onRetry(failedCount, exception!!))) {
+            try {
+                result.complete(lottieComposition(context, spec))
+            } catch (e: Throwable) {
+                exception = e
+                failedCount++
+            }
+        }
+        if (!result.isComplete && exception != null) {
+            result.completeExceptionally(exception)
+        }
+    }
+    return result
+}
+
+private suspend fun lottieComposition(
+    context: Context,
+    spec: LottieCompositionSpec
+): LottieComposition = suspendCancellableCoroutine { cont ->
+    val task = when (spec) {
+        is LottieCompositionSpec.RawRes -> LottieCompositionFactory.fromRawRes(context, spec.resId)
+        is LottieCompositionSpec.Url -> LottieCompositionFactory.fromUrl(context, spec.url, null)
+        is LottieCompositionSpec.File -> {
+            val fis = FileInputStream(spec.fileName)
+            when {
+                spec.fileName.endsWith("zip") -> LottieCompositionFactory.fromZipStream(ZipInputStream(fis), spec.fileName)
+                else -> LottieCompositionFactory.fromJsonInputStream(fis, spec.fileName)
+            }
+        }
+        is LottieCompositionSpec.Asset -> LottieCompositionFactory.fromAsset(context, spec.assetName)
+        is LottieCompositionSpec.JsonString -> LottieCompositionFactory.fromJsonString(spec.jsonString, spec.cacheKey)
+    }
+    task.addListener { c ->
+        if (!cont.isCompleted) cont.resume(c)
+    }.addFailureListener { e ->
+        if (!cont.isCompleted) cont.resumeWithException(e)
+    }
+}
diff --git a/lottie-compose/src/test/java/com/airbnb/lottie/compose/CompositionFixtures.kt b/lottie-compose/src/test/java/com/airbnb/lottie/compose/CompositionFixtures.kt
new file mode 100644
index 0000000..7e8ce59
--- /dev/null
+++ b/lottie-compose/src/test/java/com/airbnb/lottie/compose/CompositionFixtures.kt
@@ -0,0 +1,5 @@
+package com.airbnb.lottie.compose
+
+object CompositionFixtures {
+    val Rect = "{\"assets\":[],\"layers\":[{\"ddd\":0,\"ind\":0,\"ty\":4,\"nm\":\"Shape Layer 1\",\"parent\":1,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[100,100,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"k\":[{\"i\":{\"x\":[0.667,0.667],\"y\":[1,1]},\"o\":{\"x\":[0.333,0.333],\"y\":[0,0]},\"n\":[\"0p667_1_0p333_0\",\"0p667_1_0p333_0\"],\"t\":0,\"s\":[85.714,85.714],\"e\":[52.714,52.714]},{\"i\":{\"x\":[0.667,0.667],\"y\":[1,1]},\"o\":{\"x\":[0.333,0.333],\"y\":[0,0]},\"n\":[\"0p667_1_0p333_0\",\"0p667_1_0p333_0\"],\"t\":16,\"s\":[52.714,52.714],\"e\":[85.714,85.714]},{\"t\":35}]},\"p\":{\"k\":[0,0]},\"r\":{\"k\":0},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\"},{\"ty\":\"st\",\"c\":{\"k\":[0,0,0,1]},\"o\":{\"k\":100},\"w\":{\"k\":14},\"lc\":1,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\"},{\"ty\":\"tr\",\"p\":{\"k\":[2.198,1.099],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Rectangle 1\",\"np\":2,\"mn\":\"ADBE Vector Group\"}],\"ip\":0,\"op\":36,\"st\":0,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":1,\"ty\":1,\"nm\":\"White Solid 1\",\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[45,45,0]},\"a\":{\"k\":[100,100,0]},\"s\":{\"k\":[45,45,100]}},\"ao\":0,\"sw\":200,\"sh\":200,\"sc\":\"#ffffff\",\"ip\":0,\"op\":36,\"st\":0,\"bm\":0,\"sr\":1}],\"v\":\"4.5.4\",\"ddd\":0,\"ip\":0,\"op\":36,\"fr\":60,\"w\":90,\"h\":90}"
+}
\ No newline at end of file
diff --git a/lottie-compose/src/test/java/com/airbnb/lottie/compose/LottieAnimatableImplTest.kt b/lottie-compose/src/test/java/com/airbnb/lottie/compose/LottieAnimatableImplTest.kt
new file mode 100644
index 0000000..e207733
--- /dev/null
+++ b/lottie-compose/src/test/java/com/airbnb/lottie/compose/LottieAnimatableImplTest.kt
@@ -0,0 +1,330 @@
+package com.airbnb.lottie.compose
+
+import com.airbnb.lottie.LottieComposition
+import com.airbnb.lottie.LottieCompositionFactory
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.withContext
+import org.junit.Assert.*
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class LottieAnimatableImplTest {
+
+    private lateinit var clock: TestFrameClock
+    private lateinit var anim: LottieAnimatable
+    private lateinit var composition: LottieComposition
+    private val compositionDuration get() = composition.duration.toLong()
+
+    @Before
+    fun setup() {
+        clock = TestFrameClock()
+        anim = LottieAnimatable()
+        composition = LottieCompositionFactory.fromJsonStringSync(CompositionFixtures.Rect, null).value!!
+    }
+
+    @Test
+    fun testSingleIterationProgress() = runTest {
+        launch {
+            anim.animate(composition)
+        }
+        assertEquals(0f, anim.progress)
+        clock.frameMs(0)
+        clock.frameMs(300)
+        assertEquals(0.5f, anim.progress, 0.01f)
+        clock.frameMs(composition.duration.toLong() - 1)
+        assertFalse(anim.isAtEnd)
+        assertTrue(anim.isPlaying)
+        clock.frameMs(composition.duration.toLong())
+        assertTrue(anim.isAtEnd)
+        assertFalse(anim.isPlaying)
+    }
+
+    @Test
+    fun testJumpFromOneIterationToEndOfNext() = runTest {
+        launch {
+            anim.animate(composition, iterations = 2)
+        }
+        assertEquals(0f, anim.progress)
+        clock.frameMs(0)
+
+        assertFrame(300, progress = 0.5f, iterations = 2)
+        assertFrame(compositionDuration - 1, progress = 0.998f, iteration = 1, iterations = 2)
+        assertFrame(2 * compositionDuration, progress = 1f, iteration = 2, iterations = 2, isPlaying = false, isAtEnd = true)
+    }
+
+    @Test
+    fun testTwoIterations() = runTest {
+        launch {
+            anim.animate(composition, iterations = 2)
+        }
+        assertEquals(0f, anim.progress)
+        clock.frameMs(0)
+
+        assertFrame(300, progress = 0.5f, iterations = 2)
+        assertFrame(compositionDuration - 1, progress = 0.998f, iteration = 1, iterations = 2)
+        assertFrame(compositionDuration, progress = 0f, iteration = 2, iterations = 2)
+        assertFrame((2 * compositionDuration) - 1, progress = 0.998f, iteration = 2, iterations = 2)
+        assertFrame(2 * compositionDuration, progress = 1f, iteration = 2, iterations = 2, isPlaying = false, isAtEnd = true)
+    }
+
+    @Test
+    fun testJumpsFromOneIterationToThree() = runTest {
+        val job = launch {
+            anim.animate(composition, iterations = 3)
+        }
+        assertEquals(0f, anim.progress)
+        clock.frameMs(0)
+
+        assertFrame(2 * compositionDuration + 300, progress = 0.5f, iteration = 3, iterations = 3)
+        job.cancel()
+    }
+
+    @Test
+    fun testCancels() = runTest {
+        val job = launch {
+            anim.animate(composition)
+        }
+        assertEquals(0f, anim.progress)
+        clock.frameMs(0)
+
+        assertFrame(300, progress = 0.5f)
+        job.cancel()
+        assertFrame(compositionDuration - 1, progress = 0.5f, isPlaying = false, lastFrameNanos = 300000000L)
+    }
+
+    @Test
+    fun testReverse() = runTest {
+        launch {
+            anim.animate(composition, speed = -1f)
+        }
+        assertFrame(0, progress = 1f, speed = -1f)
+        assertFrame(200, progress = 2 / 3f, speed = -1f)
+        assertFrame(compositionDuration - 1, progress = 0.0016f, speed = -1f)
+        assertFrame(compositionDuration, progress = 0f, isAtEnd = true, isPlaying = false, speed = -1f)
+    }
+
+    @Test
+    fun testClipSpec() = runTest {
+        val clipSpec = LottieClipSpec.Progress(0.25f, 0.75f)
+        launch {
+            anim.animate(composition, clipSpec = clipSpec)
+        }
+        assertFrame(0, progress = 0.25f, clipSpec = clipSpec)
+        assertFrame(299, progress = 0.749f, clipSpec = clipSpec)
+        assertFrame(300, progress = 0.75f, isPlaying = false, isAtEnd = true, clipSpec = clipSpec)
+    }
+
+    @Test
+    fun testClipSpecWithTwoIterations() = runTest {
+        val clipSpec = LottieClipSpec.Progress(0.25f, 0.75f)
+        launch {
+            anim.animate(composition, clipSpec = clipSpec, iterations = 2)
+        }
+        assertFrame(0, progress = 0.25f, clipSpec = clipSpec, iterations = 2)
+        assertFrame(299, progress = 0.749f, clipSpec = clipSpec, iterations = 2)
+        assertFrame(598, progress = 0.748f, iteration = 2, clipSpec = clipSpec, iterations = 2)
+        assertFrame(599, progress = 0.75f, iteration = 2, isPlaying = false, isAtEnd = true, clipSpec = clipSpec, iterations = 2)
+    }
+
+    @Test
+    fun testNegativeSpeedWithClipSpec() = runTest {
+        val clipSpec = LottieClipSpec.Progress(0.25f, 0.75f)
+        launch {
+            anim.animate(composition, clipSpec = clipSpec, speed = -1f)
+        }
+        assertFrame(0, progress = 0.75f, clipSpec = clipSpec, speed = -1f)
+        assertFrame(299, progress = 0.2508f, clipSpec = clipSpec, speed = -1f)
+        assertFrame(300, progress = 0.25f, isPlaying = false, isAtEnd = true, clipSpec = clipSpec, speed = -1f)
+    }
+
+    @Test
+    fun testChangingEndClipSpec() = runTest {
+        launch {
+            anim.animate(composition)
+        }
+        assertFrame(0, progress = 0f)
+        assertFrame(300, progress = 0.5f)
+        val clipSpec = LottieClipSpec.Progress(max = 0.75f)
+        launch {
+            anim.animate(composition, clipSpec = clipSpec, continueFromPreviousAnimate = false)
+        }
+        assertFrame(316, progress = 0f, clipSpec = clipSpec)
+        assertFrame(616, progress = 0.5f, clipSpec = clipSpec)
+        assertFrame(800, progress = 0.75f, clipSpec = clipSpec, isPlaying = false, isAtEnd = true)
+    }
+
+    @Test
+    fun testChangingBeginningClipSpec() = runTest {
+        launch {
+            anim.animate(composition, iterations = 2)
+        }
+        assertFrame(0, progress = 0f, iterations = 2)
+        assertFrame(300, progress = 0.5f, iterations = 2)
+        val clipSpec = LottieClipSpec.Progress(min = 0.25f)
+        launch {
+            anim.animate(composition, clipSpec = clipSpec, initialProgress = anim.progress, continueFromPreviousAnimate = true)
+        }
+        assertFrame(598, progress = 0.998f, clipSpec = clipSpec, iterations = 2)
+        assertFrame(599, progress = 0.25f, clipSpec = clipSpec, iteration = 2, iterations = 2)
+        assertFrame(1048, progress = 0.999f, clipSpec = clipSpec, iteration = 2, iterations = 2)
+        assertFrame(1049, progress = 1f, clipSpec = clipSpec, iteration = 2, iterations = 2, isPlaying = false, isAtEnd = true)
+    }
+
+    @Test
+    fun testResumingAnimation() = runTest {
+        launch {
+            anim.animate(composition)
+        }
+        assertFrame(0, progress = 0f)
+        assertFrame(300, progress = 0.5f)
+        val clipSpec = LottieClipSpec.Progress(max = 0.75f)
+        launch {
+            anim.animate(
+                composition,
+                clipSpec = clipSpec,
+                initialProgress = anim.progress,
+                continueFromPreviousAnimate = true,
+            )
+        }
+        assertFrame(316, progress = 0.528f, clipSpec = clipSpec)
+        assertFrame(449, progress = 0.749f, clipSpec = clipSpec)
+        assertFrame(450, progress = 0.75f, clipSpec = clipSpec, isPlaying = false, isAtEnd = true)
+    }
+
+    @Test
+    fun testReRunAnimation() = runTest {
+        launch {
+            anim.animate(composition)
+        }
+        assertFrame(0, progress = 0f)
+        assertFrame(300, progress = 0.5f)
+        launch {
+            anim.animate(composition, initialProgress = anim.progress, continueFromPreviousAnimate = true)
+        }
+        assertFrame(300, progress = 0.5f)
+        assertFrame(598, progress = 0.998f)
+        assertFrame(599, progress = 1f, isPlaying = false, isAtEnd = true)
+    }
+
+    @Test
+    fun testSnapNoopToThenResume() = runTest {
+        launch {
+            anim.animate(composition)
+        }
+        assertFrame(0, progress = 0f)
+        assertFrame(300, progress = 0.5f)
+        launch {
+            anim.snapTo(composition)
+            anim.animate(composition, initialProgress = anim.progress, continueFromPreviousAnimate = true)
+        }
+        assertFrame(300, progress = 0.5f)
+        assertFrame(598, progress = 0.998f)
+        assertFrame(599, progress = 1f, isPlaying = false, isAtEnd = true)
+    }
+
+    @Test
+    fun testSnapToThenResume() = runTest {
+        launch {
+            anim.animate(composition)
+        }
+        assertFrame(0, progress = 0f)
+        assertFrame(300, progress = 0.5f)
+        launch {
+            anim.snapTo(composition, progress = 0.2f)
+            anim.animate(composition, initialProgress = anim.progress, continueFromPreviousAnimate = true)
+        }
+        assertFrame(316, progress = 0.2f)
+        assertFrame(449, progress = 0.422f)
+        assertFrame(795, progress = 0.999f)
+        assertFrame(796, progress = 1f, isPlaying = false, isAtEnd = true)
+    }
+
+    @Test
+    fun testSnapToAnotherIterationThenResume() = runTest {
+        launch {
+            anim.animate(composition, iterations = 3)
+        }
+        assertFrame(0, progress = 0f, iterations = 3)
+        assertFrame(1796, progress = 0.998f, iteration = 3, iterations = 3)
+        launch {
+            anim.snapTo(iteration = 1)
+            anim.animate(composition, initialProgress = anim.progress, continueFromPreviousAnimate = true)
+        }
+        assertFrame(1796, progress = 0.998f, iteration = 1, iterations = 3)
+        assertFrame(1797, progress = 0f, iteration = 2, iterations = 3)
+        assertFrame(2994, progress = 0.998f, iteration = 3, iterations = 3)
+        assertFrame(2995, progress = 1f, isPlaying = false, isAtEnd = true, iteration = 3, iterations = 3)
+    }
+
+    @Test
+    fun testChangeSpeed() = runTest {
+        launch {
+            anim.animate(composition)
+        }
+        assertFrame(0, progress = 0f)
+        assertFrame(300, progress = 0.5f)
+        launch {
+            anim.animate(composition, speed = 2f, initialProgress = anim.progress, continueFromPreviousAnimate = true)
+        }
+        assertFrame(316, progress = 0.554f, speed = 2f)
+        assertFrame(449, progress = 0.998f, speed = 2f)
+        assertFrame(450, progress = 1f, speed = 2f, isPlaying = false, isAtEnd = true)
+    }
+
+    @Test
+    fun testNonCancellable() = runTest {
+        val job = launch {
+            anim.animate(composition, cancellationBehavior = LottieCancellationBehavior.OnIterationFinish)
+        }
+        assertFrame(0, progress = 0f)
+        job.cancel()
+        assertFrame(300, progress = 0.5f)
+        assertFrame(599, progress = 1f, isAtEnd = true, isPlaying = false)
+    }
+
+    @Test
+    fun testCancelWithMultipleIterations() = runTest {
+        val job = launch {
+            anim.animate(composition, cancellationBehavior = LottieCancellationBehavior.OnIterationFinish, iterations = 3)
+        }
+        assertFrame(0, progress = 0f, iterations = 3)
+        job.cancel()
+        assertFrame(300, progress = 0.5f, iterations = 3)
+        assertFrame(599, progress = 1f, isAtEnd = false, isPlaying = false, iterations = 3)
+    }
+
+    private suspend fun assertFrame(
+        frameTimeMs: Long,
+        progress: Float,
+        iteration: Int = 1,
+        iterations: Int = 1,
+        speed: Float = 1f,
+        clipSpec: LottieClipSpec? = null,
+        isAtEnd: Boolean = false,
+        isPlaying: Boolean = true,
+        lastFrameNanos: Long = frameTimeMs * 1_000_000,
+    ) {
+        clock.frameMs(frameTimeMs)
+        assertEquals("progress at %d".format(frameTimeMs), progress, anim.progress, 0.001f)
+        assertEquals("iteration at %d".format(frameTimeMs), iteration, anim.iteration)
+        assertEquals("iterations at %d".format(frameTimeMs), iterations, anim.iterations)
+        assertEquals("speed at %d".format(frameTimeMs), speed, anim.speed)
+        assertEquals("clipSpec at %d".format(frameTimeMs), clipSpec, anim.clipSpec)
+        assertEquals("isAtEnd at %d".format(frameTimeMs), isAtEnd, anim.isAtEnd)
+        assertEquals("isPlaying at %d".format(frameTimeMs), isPlaying, anim.isPlaying)
+        assertEquals("lastFrameNanos at %d".format(frameTimeMs), lastFrameNanos, anim.lastFrameNanos)
+    }
+
+    private fun runTest(test: suspend CoroutineScope.() -> Unit) {
+        runBlockingTest {
+            withContext(clock) {
+                test()
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/lottie-compose/src/test/java/com/airbnb/lottie/compose/LottieClipSpecTest.kt b/lottie-compose/src/test/java/com/airbnb/lottie/compose/LottieClipSpecTest.kt
new file mode 100644
index 0000000..8457f54
--- /dev/null
+++ b/lottie-compose/src/test/java/com/airbnb/lottie/compose/LottieClipSpecTest.kt
@@ -0,0 +1,144 @@
+package com.airbnb.lottie.compose
+
+import android.graphics.Rect
+import androidx.collection.LongSparseArray
+import androidx.collection.SparseArrayCompat
+import com.airbnb.lottie.LottieComposition
+import com.airbnb.lottie.model.Marker
+import org.junit.Assert.*
+import org.junit.Test
+
+class LottieClipSpecTest {
+
+    @Test
+    fun testMinFrame() {
+        val spec = LottieClipSpec.Frame(min = 20)
+        val composition = createComposition(endFrame = 40f)
+        assertEquals(0.5f, spec.getMinProgress(composition))
+        assertEquals(1f, spec.getMaxProgress(composition))
+    }
+
+    @Test
+    fun testMaxFrame() {
+        val spec = LottieClipSpec.Frame(max = 20)
+        val composition = createComposition(endFrame = 40f)
+        assertEquals(0f, spec.getMinProgress(composition))
+        assertEquals(0.5f, spec.getMaxProgress(composition))
+    }
+
+    @Test
+    fun testMaxFrameNotInclusive() {
+        val spec = LottieClipSpec.Frame(max = 20, maxInclusive = false)
+        val composition = createComposition(endFrame = 40f)
+        assertEquals(0f, spec.getMinProgress(composition))
+        assertEquals(0.475f, spec.getMaxProgress(composition))
+    }
+
+    @Test
+    fun testMinAndMaxFrame() {
+        val spec = LottieClipSpec.Frame(min = 20, max = 30)
+        val composition = createComposition(endFrame = 40f)
+        assertEquals(0.5f, spec.getMinProgress(composition))
+        assertEquals(0.75f, spec.getMaxProgress(composition))
+    }
+
+    @Test
+    fun testMinAndMaxFrameNotExclusive() {
+        val spec = LottieClipSpec.Frame(min = 20, max = 30, maxInclusive = false)
+        val composition = createComposition(endFrame = 40f)
+        assertEquals(0.5f, spec.getMinProgress(composition))
+        assertEquals(0.725f, spec.getMaxProgress(composition))
+    }
+
+    @Test
+    fun testMinProgress() {
+        val spec = LottieClipSpec.Progress(min = 0.5f)
+        val composition = createComposition(endFrame = 40f)
+        assertEquals(0.5f, spec.getMinProgress(composition))
+        assertEquals(1f, spec.getMaxProgress(composition))
+    }
+
+    @Test
+    fun testMaxProgress() {
+        val spec = LottieClipSpec.Progress(max = 0.5f)
+        val composition = createComposition(endFrame = 40f)
+        assertEquals(0f, spec.getMinProgress(composition))
+        assertEquals(0.5f, spec.getMaxProgress(composition))
+    }
+
+    @Test
+    fun testMinAndMaxProgress() {
+        val spec = LottieClipSpec.Progress(min = 0.5f, max = 0.75f)
+        val composition = createComposition(endFrame = 40f)
+        assertEquals(0.5f, spec.getMinProgress(composition))
+        assertEquals(0.75f, spec.getMaxProgress(composition))
+    }
+
+    @Test
+    fun testMinMarker() {
+        val spec = LottieClipSpec.Markers(min = "start")
+        val composition = createComposition(endFrame = 40f, listOf(Marker("start", 20f, 10f)))
+        assertEquals(0.5f, spec.getMinProgress(composition))
+        assertEquals(1f, spec.getMaxProgress(composition))
+    }
+
+    @Test
+    fun testMaxMarker() {
+        val spec = LottieClipSpec.Markers(max = "end")
+        val composition = createComposition(endFrame = 40f, listOf(Marker("end", 20f, 10f)))
+        assertEquals(0f, spec.getMinProgress(composition))
+        assertEquals(0.5f, spec.getMaxProgress(composition))
+    }
+
+
+    @Test
+    fun testMaxMarkerExclusive() {
+        val spec = LottieClipSpec.Markers(max = "end", maxInclusive = false)
+        val composition = createComposition(endFrame = 40f, listOf(Marker("end", 20f, 10f)))
+        assertEquals(0f, spec.getMinProgress(composition))
+        assertEquals(0.475f, spec.getMaxProgress(composition))
+    }
+
+
+    @Test
+    fun testMinAndMaxMarker() {
+        val spec = LottieClipSpec.Markers(min = "start", max = "end")
+        val composition = createComposition(endFrame = 40f, listOf(Marker("start", 20f, 10f), Marker("end", 30f, 10f)))
+        assertEquals(0.5f, spec.getMinProgress(composition))
+        assertEquals(0.75f, spec.getMaxProgress(composition))
+    }
+
+    @Test
+    fun testMinAndMaxMarkerExclusive() {
+        val spec = LottieClipSpec.Markers(min = "start", max = "end", maxInclusive = false)
+        val composition = createComposition(endFrame = 40f, listOf(Marker("start", 20f, 10f), Marker("end", 30f, 10f)))
+        assertEquals(0.5f, spec.getMinProgress(composition))
+        assertEquals(0.725f, spec.getMaxProgress(composition))
+    }
+
+    @Test
+    fun testMarker() {
+        val spec = LottieClipSpec.Marker("span")
+        val composition = createComposition(endFrame = 40f, listOf(Marker("span", 20f, 10f)))
+        assertEquals(0.5f, spec.getMinProgress(composition))
+        assertEquals(0.75f, spec.getMaxProgress(composition))
+    }
+
+    private fun createComposition(endFrame: Float, markers: List<Marker> = emptyList()): LottieComposition {
+        val composition = LottieComposition()
+        composition.init(
+            Rect(),
+            0f,
+            endFrame,
+            30f,
+            emptyList(),
+            LongSparseArray(),
+            emptyMap(),
+            emptyMap(),
+            SparseArrayCompat(),
+            emptyMap(),
+            markers,
+        )
+        return composition
+    }
+}
\ No newline at end of file
diff --git a/lottie-compose/src/test/java/com/airbnb/lottie/compose/LottieCompositionResultImplTest.kt b/lottie-compose/src/test/java/com/airbnb/lottie/compose/LottieCompositionResultImplTest.kt
new file mode 100644
index 0000000..1192fe9
--- /dev/null
+++ b/lottie-compose/src/test/java/com/airbnb/lottie/compose/LottieCompositionResultImplTest.kt
@@ -0,0 +1,68 @@
+package com.airbnb.lottie.compose
+
+import com.airbnb.lottie.LottieComposition
+import com.airbnb.lottie.LottieCompositionFactory
+import org.junit.Assert.*
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class LottieCompositionResultImplTest {
+
+    private lateinit var composition: LottieComposition
+
+    @Before
+    fun setup() {
+        composition = LottieCompositionFactory.fromJsonStringSync(CompositionFixtures.Rect, null).value!!
+    }
+
+    @Test
+    fun testLoading() {
+        val result = LottieCompositionResultImpl()
+        assertTrue(result.isLoading)
+    }
+
+    @Test
+    fun testFail() {
+        val result = LottieCompositionResultImpl()
+        val e = IllegalStateException("Fail")
+        result.completeExceptionally(e)
+        assertFalse(result.isSuccess)
+        assertTrue(result.isFailure)
+        assertNull(result.value)
+        assertEquals(e, result.error)
+    }
+
+    @Test
+    fun testCompleted() {
+        val result = LottieCompositionResultImpl()
+        result.complete(composition)
+        assertFalse(result.isFailure)
+        assertTrue(result.isSuccess)
+        assertEquals(composition, result.value)
+    }
+
+    @Test
+    fun testCompletedThenFail() {
+        val result = LottieCompositionResultImpl()
+        result.complete(composition)
+        result.completeExceptionally(IllegalStateException("Fail"))
+        assertFalse(result.isFailure)
+        assertTrue(result.isSuccess)
+        assertEquals(composition, result.value)
+    }
+
+    @Test
+    fun testErrorThenCompleted() {
+        val result = LottieCompositionResultImpl()
+        val e = IllegalStateException("Fail")
+        result.completeExceptionally(e)
+        result.complete(composition)
+        assertFalse(result.isSuccess)
+        assertTrue(result.isFailure)
+        assertNull(result.value)
+        assertEquals(e, result.error)
+    }
+}
\ No newline at end of file
diff --git a/lottie-compose/src/test/java/com/airbnb/lottie/compose/TestFrameClock.kt b/lottie-compose/src/test/java/com/airbnb/lottie/compose/TestFrameClock.kt
new file mode 100644
index 0000000..267abc2
--- /dev/null
+++ b/lottie-compose/src/test/java/com/airbnb/lottie/compose/TestFrameClock.kt
@@ -0,0 +1,25 @@
+package com.airbnb.lottie.compose
+
+import androidx.compose.runtime.MonotonicFrameClock
+import kotlinx.coroutines.channels.Channel
+
+/**
+ * This class is original from:
+ * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/animation/animation-core/src/test/java/androidx/compose/animation/core/SuspendAnimationTest.kt;l=211
+ */
+class TestFrameClock : MonotonicFrameClock {
+    // Make the send non-blocking
+    private val frameChannel = Channel<Long>(Channel.UNLIMITED)
+
+    private suspend fun frame(frameTimeNanos: Long) {
+        frameChannel.send(frameTimeNanos)
+    }
+
+    suspend fun frameMs(frameTimeMs: Long) {
+        frame(frameTimeMs * 1_000_000L)
+    }
+
+    override suspend fun <R> withFrameNanos(onFrame: (Long) -> R): R {
+        return onFrame(frameChannel.receive())
+    }
+}
\ No newline at end of file
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java b/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java
index 6dfd5fa..393677b 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java
@@ -17,6 +17,7 @@
 import com.airbnb.lottie.model.layer.Layer;
 import com.airbnb.lottie.parser.moshi.JsonReader;
 import com.airbnb.lottie.utils.Logger;
+import com.airbnb.lottie.utils.MiscUtils;
 
 import org.json.JSONObject;
 
@@ -150,6 +151,16 @@
     return endFrame;
   }
 
+  public float getFrameForProgress(float progress) {
+    return MiscUtils.lerp(startFrame, endFrame, progress);
+  }
+
+  public float getProgressForFrame(float frame) {
+    float framesSinceStart = frame - startFrame;
+    float frameRange = endFrame - startFrame;
+    return framesSinceStart / frameRange;
+  }
+
   public float getFrameRate() {
     return frameRate;
   }
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
index 8339a33..4ef2f0f 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
@@ -818,7 +818,7 @@
       return;
     }
     L.beginSection("Drawable#setProgress");
-    animator.setFrame(MiscUtils.lerp(composition.getStartFrame(), composition.getEndFrame(), progress));
+    animator.setFrame(composition.getFrameForProgress(progress));
     L.endSection("Drawable#setProgress");
   }
 
diff --git a/lottie/src/main/java/com/airbnb/lottie/utils/LottieValueAnimator.java b/lottie/src/main/java/com/airbnb/lottie/utils/LottieValueAnimator.java
index c3a652b..1556ac6 100644
--- a/lottie/src/main/java/com/airbnb/lottie/utils/LottieValueAnimator.java
+++ b/lottie/src/main/java/com/airbnb/lottie/utils/LottieValueAnimator.java
@@ -174,9 +174,13 @@
     }
     float compositionMinFrame = composition == null ? -Float.MAX_VALUE : composition.getStartFrame();
     float compositionMaxFrame = composition == null ? Float.MAX_VALUE : composition.getEndFrame();
-    this.minFrame = MiscUtils.clamp(minFrame, compositionMinFrame, compositionMaxFrame);
-    this.maxFrame = MiscUtils.clamp(maxFrame, compositionMinFrame, compositionMaxFrame);
-    setFrame((int) MiscUtils.clamp(frame, minFrame, maxFrame));
+    float newMinFrame = MiscUtils.clamp(minFrame, compositionMinFrame, compositionMaxFrame);
+    float newMaxFrame = MiscUtils.clamp(maxFrame, compositionMinFrame, compositionMaxFrame);
+    if (newMinFrame != this.minFrame || newMaxFrame != this.maxFrame) {
+      this.minFrame = newMinFrame;
+      this.maxFrame = newMaxFrame;
+      setFrame((int) MiscUtils.clamp(frame, newMinFrame, newMaxFrame));
+    }
   }
 
   public void reverseAnimationSpeed() {
diff --git a/lottie/src/test/java/com/airbnb/lottie/LottieCompositionTest.java b/lottie/src/test/java/com/airbnb/lottie/LottieCompositionTest.java
new file mode 100644
index 0000000..671ac10
--- /dev/null
+++ b/lottie/src/test/java/com/airbnb/lottie/LottieCompositionTest.java
@@ -0,0 +1,29 @@
+package com.airbnb.lottie;
+
+import static junit.framework.TestCase.assertEquals;
+
+import org.junit.Test;
+
+public class LottieCompositionTest extends BaseTest {
+  private static final String JSON = "{\"v\":\"4.11.1\",\"fr\":60,\"ip\":0,\"op\":180,\"w\":300,\"h\":300,\"nm\":\"Comp 1\",\"ddd\":0,\"assets\":[]," +
+      "\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"Shape Layer 1\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0," +
+      "\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[150,150,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100]," +
+      "\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[100,100],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3}," +
+      "\"r\":{\"a\":0,\"k\":0,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\"," +
+      "\"c\":{\"a\":0,\"k\":[0.928262987324,0,0,1],\"ix\":4},\"o\":{\"a\":0,\"k\":100,\"ix\":5},\"r\":1,\"nm\":\"Fill 1\",\"mn\":\"ADBE Vector " +
+      "Graphic - Fill\",\"hd\":false}],\"ip\":0,\"op\":180,\"st\":0,\"bm\":0}]}";
+
+  @Test
+  public void testGetFrameForProgress() {
+    LottieResult<LottieComposition> result = LottieCompositionFactory.fromJsonStringSync(JSON, null);
+    //noinspection ConstantConditions
+    assertEquals(66.59f, result.getValue().getFrameForProgress(0.37f), 0.01f);
+  }
+
+  @Test
+  public void testGetProgressForFrame() {
+    LottieResult<LottieComposition> result = LottieCompositionFactory.fromJsonStringSync(JSON, null);
+    //noinspection ConstantConditions
+    assertEquals(0.5f, result.getValue().getProgressForFrame(90), 0.01f);
+  }
+}
diff --git a/sample-compose/build.gradle b/sample-compose/build.gradle
index c05afb8..72a88c4 100644
--- a/sample-compose/build.gradle
+++ b/sample-compose/build.gradle
@@ -35,6 +35,7 @@
         "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
         "-Xuse-experimental=androidx.compose.animation.ExperimentalAnimationApi",
         "-Xopt-in=androidx.compose.material.ExperimentalMaterialApi",
+        "-Xopt-in=com.google.accompanist.pager.ExperimentalPagerApi",
         "-Xopt-in=kotlin.RequiresOptIn",
     ]
   }
@@ -77,6 +78,7 @@
   implementation 'com.squareup.retrofit2:retrofit:2.9.0'
   implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
   implementation "com.google.accompanist:accompanist-coil:0.11.1"
+  implementation "com.google.accompanist:accompanist-pager-indicators:0.11.1"
   implementation 'com.airbnb.android:mavericks:2.3.0'
   implementation 'com.airbnb.android:mavericks-compose:2.1.0-alpha02'
 
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/ComposeActivity.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/ComposeActivity.kt
index 904173b..a810e90 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/ComposeActivity.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/ComposeActivity.kt
@@ -7,7 +7,11 @@
 import androidx.appcompat.app.AppCompatActivity
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.padding
-import androidx.compose.material.*
+import androidx.compose.material.BottomNavigation
+import androidx.compose.material.BottomNavigationItem
+import androidx.compose.material.Icon
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
@@ -15,8 +19,17 @@
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
-import androidx.navigation.compose.*
-import com.airbnb.lottie.compose.LottieAnimationSpec
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.currentBackStackEntryAsState
+import androidx.navigation.compose.rememberNavController
+import com.airbnb.lottie.compose.LottieCompositionSpec
+import com.airbnb.lottie.sample.compose.examples.AnimatableExamplesPage
+import com.airbnb.lottie.sample.compose.examples.BasicUsageExamplesPage
+import com.airbnb.lottie.sample.compose.examples.ExamplesPage
+import com.airbnb.lottie.sample.compose.examples.NetworkExamplesPage
+import com.airbnb.lottie.sample.compose.examples.TransitionsExamplesPage
+import com.airbnb.lottie.sample.compose.examples.ViewPagerExamplePage
 import com.airbnb.lottie.sample.compose.lottiefiles.LottieFilesPage
 import com.airbnb.lottie.sample.compose.player.PlayerPage
 import com.airbnb.lottie.sample.compose.preview.PreviewPage
@@ -74,19 +87,24 @@
                     modifier = Modifier.padding(innerPadding)
                 ) {
                     NavHost(navController, startDestination = Route.Showcase.route) {
-                        composable(Route.Showcase.route, arguments = Route.Showcase.args) { ShowcasePage(navController) }
-                        composable(Route.Preview.route, arguments = Route.Preview.args) { PreviewPage(navController) }
-                        composable(Route.LottieFiles.route, arguments = Route.LottieFiles.args) { LottieFilesPage(navController) }
-                        composable(Route.Learn.route, arguments = Route.Learn.args) { ShowcasePage(navController) }
+                        composable(Route.Showcase.route) { ShowcasePage(navController) }
+                        composable(Route.Preview.route) { PreviewPage(navController) }
+                        composable(Route.LottieFiles.route) { LottieFilesPage(navController) }
+                        composable(Route.Examples.route) { ExamplesPage(navController) }
+                        composable(Route.BasicUsageExamples.route) { BasicUsageExamplesPage() }
+                        composable(Route.AnimatableUsageExamples.route) { AnimatableExamplesPage() }
+                        composable(Route.TransitionsExamples.route) { TransitionsExamplesPage() }
+                        composable(Route.ViewPagerExample.route) { ViewPagerExamplePage() }
+                        composable(Route.NetworkExamples.route) { NetworkExamplesPage() }
                         composable(
                             Route.Player.fullRoute,
                             arguments = Route.Player.args
                         ) { entry ->
                             val arguments = entry.arguments ?: error("No arguments provided to ${Route.Player}")
                             val spec = when {
-                                arguments.getString("url") != null -> LottieAnimationSpec.Url(arguments.getBase64String("url"))
-                                arguments.getString("file") != null -> LottieAnimationSpec.File(arguments.getBase64String("file"))
-                                arguments.getString("asset") != null -> LottieAnimationSpec.Asset(arguments.getBase64String("asset"))
+                                arguments.getString("url") != null -> LottieCompositionSpec.Url(arguments.getBase64String("url"))
+                                arguments.getString("file") != null -> LottieCompositionSpec.File(arguments.getBase64String("file"))
+                                arguments.getString("asset") != null -> LottieCompositionSpec.Asset(arguments.getBase64String("asset"))
                                 else -> error("You must specify a url, file, or asset")
                             }
                             val backgroundColor = when (arguments.getString("backgroundColor") != null) {
@@ -106,6 +124,6 @@
         Showcase(Route.Showcase, R.drawable.ic_showcase, R.string.bottom_tab_showcase),
         Preview(Route.Preview, R.drawable.ic_device, R.string.bottom_tab_preview),
         LottieFiles(Route.LottieFiles, R.drawable.ic_lottie_files, R.string.bottom_tab_lottie_files),
-        Docs(Route.Learn, R.drawable.ic_docs, R.string.bottom_tab_docs),
+        Docs(Route.Examples, R.drawable.ic_examples, R.string.bottom_tab_examples),
     }
 }
\ No newline at end of file
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/Route.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/Route.kt
index 80de589..daa5f19 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/Route.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/Route.kt
@@ -1,9 +1,12 @@
 package com.airbnb.lottie.sample.compose
 
 import android.util.Base64
+import androidx.navigation.NavController
 import androidx.navigation.compose.NamedNavArgument
 import androidx.navigation.compose.navArgument
 
+fun NavController.navigate(route: Route) = navigate(route.route)
+
 sealed class Route(val route: String, val args: List<NamedNavArgument> = emptyList()) {
     object Showcase : Route("showcase")
 
@@ -11,7 +14,17 @@
 
     object LottieFiles : Route("lottiefiles")
 
-    object Learn : Route("learn")
+    object Examples : Route("examples")
+
+    object BasicUsageExamples : Route("basic usage examples")
+
+    object AnimatableUsageExamples : Route("LottieAnimatable examples")
+
+    object TransitionsExamples : Route("transitions examples")
+
+    object ViewPagerExample : Route("view pager example")
+
+    object NetworkExamples : Route("network examples")
 
     object Player : Route(
         "player",
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/composables/AnimationRow.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/composables/AnimationRow.kt
index 531538b..9743765 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/composables/AnimationRow.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/composables/AnimationRow.kt
@@ -2,7 +2,6 @@
 
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
@@ -26,8 +25,7 @@
     onClick: () -> Unit
 ) {
     Surface(
-        modifier = Modifier
-            .clickable(onClick = onClick)
+        onClick = onClick,
     ) {
         Row(
             verticalAlignment = Alignment.CenterVertically,
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/composables/Loader.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/composables/Loader.kt
index 722bdcf..2d5105f 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/composables/Loader.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/composables/Loader.kt
@@ -2,22 +2,22 @@
 
 import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 import com.airbnb.lottie.compose.LottieAnimation
-import com.airbnb.lottie.compose.LottieAnimationSpec
+import com.airbnb.lottie.compose.LottieCompositionSpec
+import com.airbnb.lottie.compose.rememberLottieComposition
 import com.airbnb.lottie.sample.compose.R
 
 @Composable
 fun Loader(
     modifier: Modifier = Modifier
 ) {
-    val animationSpec = remember { LottieAnimationSpec.RawRes(R.raw.loading) }
+    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.loading))
     LottieAnimation(
-        animationSpec,
-        modifier = Modifier
+        composition,
+        modifier = modifier
             .size(100.dp)
-            .then(modifier)
     )
 }
\ No newline at end of file
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/AnimatableExamplesPage.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/AnimatableExamplesPage.kt
new file mode 100644
index 0000000..562b837
--- /dev/null
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/AnimatableExamplesPage.kt
@@ -0,0 +1,169 @@
+package com.airbnb.lottie.sample.compose.examples
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Slider
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import com.airbnb.lottie.compose.rememberLottieAnimatable
+import com.airbnb.lottie.compose.LottieAnimation
+import com.airbnb.lottie.compose.LottieCompositionSpec
+import com.airbnb.lottie.compose.LottieConstants
+import com.airbnb.lottie.compose.rememberLottieComposition
+import com.airbnb.lottie.sample.compose.R
+
+@Composable
+fun AnimatableExamplesPage() {
+    UsageExamplePageScaffold {
+        Column(
+            modifier = Modifier
+                .fillMaxWidth()
+                .verticalScroll(rememberScrollState())
+        ) {
+            ExampleCard("Example 1", "Repeat Forever") {
+                Example1()
+            }
+            ExampleCard("Example 2", "Draggable Progress Slider") {
+                Example2()
+            }
+            ExampleCard("Example 3", "Draggable Speed Slider") {
+                Example3()
+            }
+            ExampleCard("Example 4", "Repeat once. Click to repeat again") {
+                Example4()
+            }
+            ExampleCard("Example 5", "Click to toggle playback") {
+                Example5()
+            }
+        }
+    }
+}
+
+@Composable
+private fun Example1() {
+    val anim = rememberLottieAnimatable()
+    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
+    LaunchedEffect(composition) {
+        anim.animate(
+            composition,
+            iterations = LottieConstants.IterateForever,
+        )
+    }
+    LottieAnimation(anim.composition, anim.progress)
+}
+
+@Composable
+private fun Example2() {
+    val anim = rememberLottieAnimatable()
+    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
+    var sliderGestureProgress: Float? by remember { mutableStateOf(null) }
+    LaunchedEffect(composition, sliderGestureProgress) {
+        when (val p = sliderGestureProgress) {
+            null -> anim.animate(
+                composition,
+                iterations = LottieConstants.IterateForever,
+                initialProgress = anim.progress,
+                continueFromPreviousAnimate = false,
+            )
+            else -> anim.snapTo(progress = p)
+        }
+    }
+    Box {
+        LottieAnimation(anim.composition, anim.progress)
+        Slider(
+            value = sliderGestureProgress ?: anim.progress,
+            onValueChange = { sliderGestureProgress = it },
+            onValueChangeFinished = { sliderGestureProgress = null },
+            modifier = Modifier
+                .align(Alignment.BottomCenter)
+                .padding(bottom = 8.dp)
+        )
+    }
+}
+
+@Composable
+private fun Example3() {
+    val anim = rememberLottieAnimatable()
+    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
+    var speed by remember { mutableStateOf(1f) }
+    LaunchedEffect(composition, speed) {
+        anim.animate(
+            composition,
+            iterations = LottieConstants.IterateForever,
+            speed = speed,
+            initialProgress = anim.progress,
+        )
+    }
+    Box {
+        LottieAnimation(composition, anim.progress)
+        Slider(
+            value = speed,
+            onValueChange = { speed = it },
+            valueRange = -3f..3f,
+            modifier = Modifier
+                .align(Alignment.BottomCenter)
+                .padding(bottom = 8.dp)
+        )
+        Box(
+            modifier = Modifier
+                .align(Alignment.BottomCenter)
+                .padding(bottom = 24.dp)
+                .size(width = 1.dp, height = 16.dp)
+                .background(Color.Black)
+        )
+    }
+}
+
+@Composable
+private fun Example4() {
+    var nonce by remember { mutableStateOf(1) }
+    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
+    val animatable = rememberLottieAnimatable()
+
+    LaunchedEffect(composition, nonce) {
+        composition ?: return@LaunchedEffect
+        animatable.animate(
+            composition,
+            continueFromPreviousAnimate = false,
+        )
+    }
+    LottieAnimation(
+        composition,
+        animatable.progress,
+        modifier = Modifier
+            .clickable { nonce++ }
+    )
+}
+
+@Composable
+private fun Example5() {
+    var shouldPlay by remember { mutableStateOf(true) }
+    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
+    val animatable = rememberLottieAnimatable()
+
+    LaunchedEffect(composition, shouldPlay) {
+        if (composition == null || !shouldPlay) return@LaunchedEffect
+        animatable.animate(composition, iteration = LottieConstants.IterateForever)
+    }
+    LottieAnimation(
+        composition,
+        animatable.progress,
+        modifier = Modifier
+            .clickable { shouldPlay = !shouldPlay }
+    )
+}
\ No newline at end of file
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/BasicUsageExamplesPage.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/BasicUsageExamplesPage.kt
new file mode 100644
index 0000000..d592c18
--- /dev/null
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/BasicUsageExamplesPage.kt
@@ -0,0 +1,173 @@
+package com.airbnb.lottie.sample.compose.examples
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.airbnb.lottie.compose.LottieAnimation
+import com.airbnb.lottie.compose.LottieClipSpec
+import com.airbnb.lottie.compose.LottieCompositionSpec
+import com.airbnb.lottie.compose.LottieConstants
+import com.airbnb.lottie.compose.animateLottieCompositionAsState
+import com.airbnb.lottie.compose.rememberLottieComposition
+import com.airbnb.lottie.sample.compose.R
+
+@Composable
+fun BasicUsageExamplesPage() {
+    UsageExamplePageScaffold {
+        Column(
+            modifier = Modifier
+                .fillMaxWidth()
+                .verticalScroll(rememberScrollState())
+        ) {
+            Box(modifier = Modifier.height(16.dp))
+            ExampleCard("Example 1", "Repeat once") {
+                Example1()
+            }
+            ExampleCard("Example 2", "Repeat forever") {
+                Example2()
+            }
+            ExampleCard("Example 3", "Repeat forever from 50% to 75%") {
+                Example3()
+            }
+            ExampleCard("Example 4", "Using LottieAnimationResult") {
+                Example4()
+            }
+            ExampleCard("Example 5", "Using LottieComposition") {
+                Example5()
+            }
+            ExampleCard("Example 6", "Splitting out the animation driver") {
+                Example6()
+            }
+            ExampleCard("Example 7", "Toggle on click - click me") {
+                Example7()
+            }
+        }
+    }
+}
+
+/**
+ * Nice and easy... This will play one time as soon as the composition loads
+ * then it will stop.
+ */
+@Composable
+private fun Example1() {
+    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
+    LottieAnimation(composition)
+}
+
+/**
+ * This will repeat forever.
+ */
+@Composable
+private fun Example2() {
+    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
+    LottieAnimation(
+        composition,
+        iterations = LottieConstants.IterateForever,
+    )
+}
+
+/**
+ * This will repeat between 50% and 75% forever.
+ */
+@Composable
+private fun Example3() {
+    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
+    LottieAnimation(
+        composition,
+        iterations = LottieConstants.IterateForever,
+        clipSpec = LottieClipSpec.Progress(0.5f, 0.75f),
+    )
+}
+
+/**
+ * Here, you can check the result for loading/failure states.
+ */
+@Composable
+private fun Example4() {
+    val compositionResult = rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
+    when {
+        compositionResult.isLoading -> {
+            Text("Animation is loading...")
+        }
+        compositionResult.isFailure -> {
+            Text("Animation failed to load")
+        }
+        compositionResult.isSuccess -> {
+            LottieAnimation(
+                compositionResult.value,
+                iterations = LottieConstants.IterateForever,
+            )
+        }
+    }
+}
+
+/**
+ * If you just want access to the composition itself, you can use the delegate
+ * version of lottieComposition like this.
+ */
+@Composable
+private fun Example5() {
+    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
+    LottieAnimation(
+        composition,
+        progress = 0.65f,
+    )
+}
+
+/**
+ * Here, you have access to the composition and animation individually.
+ */
+@Composable
+private fun Example6() {
+    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
+    val progress by animateLottieCompositionAsState(
+        composition,
+        iterations = LottieConstants.IterateForever,
+    )
+    LottieAnimation(
+        composition,
+        progress,
+    )
+}
+
+/**
+ * Here, you can toggle playback by clicking the animation.
+ */
+@Composable
+private fun Example7() {
+    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
+    var isPlaying by remember { mutableStateOf(false) }
+    LottieAnimation(
+        composition,
+        iterations = LottieConstants.IterateForever,
+        // When this is true, it it will start from 0 every time it is played again.
+        // When this is false, it will resume from the progress it was pause at.
+        restartOnPlay = false,
+        isPlaying = isPlaying,
+        modifier = Modifier
+            .clickable { isPlaying = !isPlaying }
+    )
+}
+
+@Preview
+@Composable
+fun ExampleCardPreview() {
+    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
+    ExampleCard("Example 1", "Heart animation") {
+        LottieAnimation(composition)
+    }
+}
\ No newline at end of file
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ExampleCard.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ExampleCard.kt
new file mode 100644
index 0000000..c073331
--- /dev/null
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ExampleCard.kt
@@ -0,0 +1,59 @@
+package com.airbnb.lottie.sample.compose.examples
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+@Composable
+fun ExampleCard(
+    name: String,
+    description: String,
+    content: @Composable () -> Unit,
+) {
+    Surface(
+        shape = RoundedCornerShape(6.dp),
+        elevation = 8.dp,
+        modifier = Modifier
+            .fillMaxWidth()
+            .wrapContentHeight()
+            .padding(bottom = 16.dp)
+            .padding(horizontal = 48.dp)
+    ) {
+        Column {
+            Box(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .aspectRatio(1f)
+                    .background(Color.Gray)
+            ) {
+                content()
+            }
+            Text(
+                name,
+                modifier = Modifier
+                    .padding(horizontal = 16.dp)
+                    .padding(top = 16.dp, bottom = 4.dp)
+            )
+            Text(
+                description,
+                color = Color.Gray,
+                fontSize = 12.sp,
+                modifier = Modifier
+                    .padding(horizontal = 16.dp)
+                    .padding(bottom = 16.dp)
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ExamplesPage.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ExamplesPage.kt
new file mode 100644
index 0000000..c8eaaa0
--- /dev/null
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ExamplesPage.kt
@@ -0,0 +1,56 @@
+package com.airbnb.lottie.sample.compose.examples
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.ListItem
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.navigation.NavController
+import com.airbnb.lottie.sample.compose.R
+import com.airbnb.lottie.sample.compose.Route
+import com.airbnb.lottie.sample.compose.composables.Marquee
+import com.airbnb.lottie.sample.compose.navigate
+
+@Composable
+fun ExamplesPage(navController: NavController) {
+    Column(
+        modifier = Modifier
+            .verticalScroll(rememberScrollState())
+    ) {
+        Marquee(stringResource(R.string.examples_title))
+        ListItem(
+            text = { Text("Basic Usage") },
+            secondaryText = { Text("Various example of simple Lottie usage") },
+            modifier = Modifier
+                .clickable { navController.navigate(Route.BasicUsageExamples) }
+        )
+        ListItem(
+            text = { Text("Animatable Usage") },
+            secondaryText = { Text("Usage of LottieAnimatable") },
+            modifier = Modifier
+                .clickable { navController.navigate(Route.AnimatableUsageExamples) }
+        )
+        ListItem(
+            text = { Text("Transitions") },
+            secondaryText = { Text("Sequencing segments of an animation based on state") },
+            modifier = Modifier
+                .clickable { navController.navigate(Route.TransitionsExamples) }
+        )
+        ListItem(
+            text = { Text("View Pager") },
+            secondaryText = { Text("Syncing a Lottie animation with a view pager") },
+            modifier = Modifier
+                .clickable { navController.navigate(Route.ViewPagerExample) }
+        )
+        ListItem(
+            text = { Text("Network Animations") },
+            secondaryText = { Text("Loading animations from a url") },
+            modifier = Modifier
+                .clickable { navController.navigate(Route.NetworkExamples) }
+        )
+    }
+}
\ No newline at end of file
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ExamplesPageScaffold.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ExamplesPageScaffold.kt
new file mode 100644
index 0000000..0661595
--- /dev/null
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ExamplesPageScaffold.kt
@@ -0,0 +1,39 @@
+package com.airbnb.lottie.sample.compose.examples
+
+import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.Scaffold
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import com.airbnb.lottie.sample.compose.R
+
+@Composable
+fun UsageExamplePageScaffold(
+    content: @Composable (padding: PaddingValues) -> Unit,
+) {
+    val backPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
+    Scaffold(
+        topBar = {
+            TopAppBar(
+                title = {},
+                navigationIcon = {
+                    IconButton(
+                        onClick = { backPressedDispatcher?.onBackPressed() },
+                    ) {
+                        Icon(
+                            Icons.Default.Close,
+                            contentDescription = stringResource(R.string.back)
+                        )
+                    }
+                },
+            )
+        }
+    ) { padding ->
+        content(padding)
+    }
+}
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/NetworkExamplesPage.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/NetworkExamplesPage.kt
new file mode 100644
index 0000000..92cdbdd
--- /dev/null
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/NetworkExamplesPage.kt
@@ -0,0 +1,79 @@
+package com.airbnb.lottie.sample.compose.examples
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import com.airbnb.lottie.compose.LottieAnimation
+import com.airbnb.lottie.compose.LottieCompositionSpec
+import com.airbnb.lottie.compose.rememberLottieComposition
+import com.airbnb.lottie.compose.rememberLottieRetrySignal
+
+@Composable
+fun NetworkExamplesPage() {
+    UsageExamplePageScaffold {
+        Column(
+            modifier = Modifier
+                .fillMaxWidth()
+                .verticalScroll(rememberScrollState())
+        ) {
+            Box(modifier = Modifier.height(16.dp))
+            ExampleCard("Example 1", "Basic URL") {
+                Example1()
+            }
+            ExampleCard("Example 2", "Fail with retries. Click to retry.") {
+                Example2()
+            }
+        }
+    }
+}
+
+@Composable
+private fun Example1() {
+    val composition by rememberLottieComposition(LottieCompositionSpec.Url("https://raw.githubusercontent.com/airbnb/lottie-android/master/sample/src/main/res/raw/heart.json"))
+    LottieAnimation(composition)
+}
+
+@Composable
+private fun Example2() {
+    val retrySignal = rememberLottieRetrySignal()
+    var failedCount by remember { mutableStateOf(0) }
+    val composition by rememberLottieComposition(
+        LottieCompositionSpec.Url("not a url"),
+        onRetry = { fc, _ ->
+            failedCount = fc
+            // Await the retry signal.
+            retrySignal.awaitRetry()
+            true
+        }
+    )
+    Box(
+        modifier = Modifier
+            .fillMaxSize()
+            .clickable { retrySignal.retry() }
+    ) {
+        LottieAnimation(composition)
+        Text(
+            "Failed $failedCount times.\nAwaiting retry: ${retrySignal.isAwaitingRetry}",
+            color = Color.LightGray,
+            modifier = Modifier
+                .padding(8.dp)
+                .align(Alignment.BottomStart)
+        )
+    }
+}
\ No newline at end of file
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/TransitionsExamplesPage.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/TransitionsExamplesPage.kt
new file mode 100644
index 0000000..e26008f
--- /dev/null
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/TransitionsExamplesPage.kt
@@ -0,0 +1,119 @@
+package com.airbnb.lottie.sample.compose.examples
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Text
+import androidx.compose.material.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.airbnb.lottie.compose.rememberLottieAnimatable
+import com.airbnb.lottie.compose.LottieAnimation
+import com.airbnb.lottie.compose.LottieCancellationBehavior
+import com.airbnb.lottie.compose.LottieClipSpec
+import com.airbnb.lottie.compose.LottieCompositionSpec
+import com.airbnb.lottie.compose.LottieConstants
+import com.airbnb.lottie.compose.rememberLottieComposition
+import com.airbnb.lottie.sample.compose.R
+import kotlinx.coroutines.NonCancellable
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.withContext
+
+enum class TransitionSection {
+    Intro,
+    LoopMiddle,
+    Outro;
+
+    fun next(): TransitionSection = when (this) {
+        Intro -> LoopMiddle
+        LoopMiddle -> Outro
+        Outro -> Intro
+    }
+}
+
+@Composable
+fun TransitionsExamplesPage() {
+    var state by remember { mutableStateOf(TransitionSection.Intro) }
+
+    UsageExamplePageScaffold { padding ->
+        Column(
+            modifier = Modifier
+                .padding(padding)
+        ) {
+            Text(
+                "Single composition",
+                modifier = Modifier
+                    .padding(8.dp)
+            )
+            SingleCompositionTransition(state)
+            Text(
+                "Multiple compositions",
+                modifier = Modifier
+                    .padding(8.dp)
+            )
+            SplitCompositionTransition(state)
+            TextButton(
+                onClick = { state = state.next() }
+            ) {
+                Text("State: $state")
+            }
+        }
+
+    }
+}
+
+@Composable
+fun SingleCompositionTransition(section: TransitionSection) {
+    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.bar))
+    val animatable = rememberLottieAnimatable()
+    val state by rememberUpdatedState(section)
+
+    LaunchedEffect(composition, animatable) {
+        composition ?: return@LaunchedEffect
+        snapshotFlow { state }.collectLatest { s ->
+            val clipSpec = when (s) {
+                TransitionSection.Intro -> LottieClipSpec.Progress(0f, 0.301f)
+                TransitionSection.LoopMiddle -> LottieClipSpec.Progress(0.301f, 2f / 3f)
+                TransitionSection.Outro -> LottieClipSpec.Progress(2f / 3f, 1f)
+            }
+            do {
+                animatable.animate(
+                    composition,
+                    clipSpec = clipSpec,
+                    cancellationBehavior = LottieCancellationBehavior.OnIterationFinish,
+                )
+            } while (s == TransitionSection.LoopMiddle)
+        }
+    }
+    LottieAnimation(composition, animatable.progress)
+}
+
+@Composable
+fun SplitCompositionTransition(section: TransitionSection) {
+    val introComposition = rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.bar_1))
+    val loopMiddleComposition = rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.bar_2))
+    val outroComposition = rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.bar_3))
+    val animatable = rememberLottieAnimatable()
+
+    LaunchedEffect(section) {
+        val composition = when (section) {
+            TransitionSection.Intro -> introComposition
+            TransitionSection.LoopMiddle -> loopMiddleComposition
+            TransitionSection.Outro -> outroComposition
+        }.await()
+        animatable.animate(
+            composition,
+            iterations = if (section == TransitionSection.LoopMiddle) LottieConstants.IterateForever else 1,
+            cancellationBehavior = LottieCancellationBehavior.OnIterationFinish,
+        )
+    }
+
+    LottieAnimation(animatable.composition, animatable.progress)
+}
\ No newline at end of file
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ViewPagerExample.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ViewPagerExample.kt
new file mode 100644
index 0000000..807214e
--- /dev/null
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ViewPagerExample.kt
@@ -0,0 +1,67 @@
+package com.airbnb.lottie.sample.compose.examples
+
+import androidx.compose.animation.core.exponentialDecay
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import com.airbnb.lottie.compose.LottieAnimation
+import com.airbnb.lottie.compose.LottieCompositionSpec
+import com.airbnb.lottie.compose.rememberLottieComposition
+import com.airbnb.lottie.sample.compose.R
+import com.google.accompanist.pager.HorizontalPager
+import com.google.accompanist.pager.HorizontalPagerIndicator
+import com.google.accompanist.pager.PagerDefaults
+import com.google.accompanist.pager.PagerState
+import com.google.accompanist.pager.rememberPagerState
+
+@Composable
+fun ViewPagerExamplePage() {
+    val colors = listOf(Color.Red, Color.Green, Color.Blue, Color.Magenta)
+    val pagerState = rememberPagerState(pageCount = colors.size)
+    Box(
+        modifier = Modifier
+            .fillMaxSize()
+    ) {
+        HorizontalPager(
+            pagerState,
+            flingBehavior = PagerDefaults.defaultPagerFlingConfig(
+                pagerState,
+                decayAnimationSpec = exponentialDecay(frictionMultiplier = 0.05f),
+            )
+        ) { page ->
+            Box(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .background(colors[page])
+            )
+        }
+        WalkthroughAnimation(pagerState)
+        HorizontalPagerIndicator(
+            pagerState,
+            modifier = Modifier
+                .align(Alignment.BottomCenter)
+                .padding(bottom = 32.dp)
+        )
+
+    }
+}
+
+@Composable
+private fun WalkthroughAnimation(pagerState: PagerState) {
+    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.walkthrough))
+    val progress by derivedStateOf { (pagerState.currentPage + pagerState.currentPageOffset) / (pagerState.pageCount - 1f) }
+    LottieAnimation(
+        composition,
+        progress,
+        modifier = Modifier
+            .fillMaxSize()
+    )
+}
\ No newline at end of file
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/lottiefiles/LottieFilesPage.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/lottiefiles/LottieFilesPage.kt
index 64ffcaf..d9d7aed 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/lottiefiles/LottieFilesPage.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/lottiefiles/LottieFilesPage.kt
@@ -5,11 +5,23 @@
 import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentWidth
 import androidx.compose.material.MaterialTheme
 import androidx.compose.material.Text
-import androidx.compose.runtime.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.onGloballyPositioned
@@ -52,16 +64,15 @@
     modifier: Modifier = Modifier
 ) {
     Row(
-        modifier = Modifier
-            .fillMaxWidth()
-            .then(modifier),
-        horizontalArrangement = Arrangement.Start
+        horizontalArrangement = Arrangement.Start,
+        modifier = modifier
+            .fillMaxWidth(),
     ) {
         for (tab in LottieFilesTab.values()) {
             LottieFilesTabBarTab(
                 text = stringResource(tab.stringRes),
                 isSelected = tab == selectedTab,
-                onClick = { onTabSelected(tab) }
+                onClick = { onTabSelected(tab) },
             )
         }
     }
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/lottiefiles/LottieFilesSearchPage.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/lottiefiles/LottieFilesSearchPage.kt
index 258e594..e27eb88 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/lottiefiles/LottieFilesSearchPage.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/lottiefiles/LottieFilesSearchPage.kt
@@ -1,13 +1,16 @@
 package com.airbnb.lottie.sample.compose.lottiefiles
 
-import android.util.Log
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.itemsIndexed
-import androidx.compose.material.*
+import androidx.compose.material.FloatingActionButton
+import androidx.compose.material.Icon
+import androidx.compose.material.OutlinedTextField
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Repeat
 import androidx.compose.runtime.Composable
@@ -83,7 +86,6 @@
         if (state.currentPage >= state.lastPage) return@withState
         fetchJob = viewModelScope.launch {
             val response = try {
-                Log.d("Gabe", "Fetching page ${state.currentPage + 1}")
                 api.search(state.query, state.currentPage + 1)
             } catch (e: Exception) {
                 setState { copy(fetchException = true) }
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/player/PlayerPage.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/player/PlayerPage.kt
index 6339dbf..8e877dc 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/player/PlayerPage.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/player/PlayerPage.kt
@@ -1,20 +1,52 @@
 package com.airbnb.lottie.sample.compose.player
 
-import androidx.activity.OnBackPressedDispatcher
 import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.expandVertically
 import androidx.compose.animation.shrinkVertically
-import androidx.compose.foundation.*
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.*
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Slider
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
 import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.*
-import androidx.compose.runtime.*
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.MergeType
+import androidx.compose.material.icons.filled.Pause
+import androidx.compose.material.icons.filled.PlayArrow
+import androidx.compose.material.icons.filled.RemoveRedEye
+import androidx.compose.material.icons.filled.Repeat
+import androidx.compose.material.icons.filled.Warning
+import androidx.compose.material.rememberScaffoldState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
@@ -32,7 +64,12 @@
 import androidx.compose.ui.window.Dialog
 import com.airbnb.lottie.ImageAssetDelegate
 import com.airbnb.lottie.LottieComposition
-import com.airbnb.lottie.compose.*
+import com.airbnb.lottie.compose.LottieAnimatable
+import com.airbnb.lottie.compose.LottieAnimation
+import com.airbnb.lottie.compose.LottieCompositionSpec
+import com.airbnb.lottie.compose.LottieConstants
+import com.airbnb.lottie.compose.rememberLottieComposition
+import com.airbnb.lottie.compose.resetToBeginning
 import com.airbnb.lottie.sample.compose.BuildConfig
 import com.airbnb.lottie.sample.compose.R
 import com.airbnb.lottie.sample.compose.composables.DebouncedCircularProgressIndicator
@@ -44,152 +81,70 @@
 import kotlin.math.ceil
 import kotlin.math.roundToInt
 
+@Stable
+class PlayerPageState(backgroundColor: Color?) {
+    val animatable = LottieAnimatable()
+
+    var backgroundColor by mutableStateOf(backgroundColor)
+    var outlineMasksAndMattes by mutableStateOf(false)
+    var applyOpacityToLayers by mutableStateOf(false)
+    var enableMergePaths by mutableStateOf(false)
+    var focusMode by mutableStateOf(false)
+    var showWarningsDialog by mutableStateOf(false)
+
+    var borderToolbar by mutableStateOf(false)
+    var speedToolbar by mutableStateOf(false)
+    var backgroundColorToolbar by mutableStateOf(false)
+
+    var progressSliderGesture: Float? by mutableStateOf(null)
+    var shouldPlay by mutableStateOf(true)
+    var targetSpeed by mutableStateOf(1f)
+    var shouldLoop by mutableStateOf(true)
+}
+
 @Composable
 fun PlayerPage(
-    spec: LottieAnimationSpec,
+    spec: LottieCompositionSpec,
     animationBackgroundColor: Color? = null,
 ) {
-    val backPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
-    val compositionResult = rememberLottieComposition(spec)
-    val dummyBitmapStrokeWidth = with(LocalDensity.current) { 3.dp.toPx() }
-    val imageAssetDelegate = remember(compositionResult) { ImageAssetDelegate { it.bitmap ?: it.toDummyBitmap(dummyBitmapStrokeWidth) }}
-    val animationState = rememberLottieAnimationState(
-        autoPlay = true,
-        repeatCount = Integer.MAX_VALUE,
-        imageAssetDelegate = imageAssetDelegate
-    )
     val scaffoldState = rememberScaffoldState()
-    val outlineMasksAndMattes = remember { mutableStateOf(false) }
-    val applyOpacityToLayers = remember { mutableStateOf(false) }
-    val enableMergePaths = remember { mutableStateOf(animationState.enableMergePaths) }
-    var focusMode by remember { mutableStateOf(false) }
-    var backgroundColor by remember { mutableStateOf(animationBackgroundColor) }
-    var showWarningsDialog by remember { mutableStateOf(false) }
-
-    val borderToolbar = remember { mutableStateOf(false) }
-    val speedToolbar = remember { mutableStateOf(false) }
-    val backgroundColorToolbar = remember { mutableStateOf(false) }
+    val state = remember { PlayerPageState(animationBackgroundColor) }
 
     val failedMessage = stringResource(R.string.failed_to_load)
     val okMessage = stringResource(R.string.ok)
 
-    LaunchedEffect(compositionResult) {
-        if (compositionResult is LottieCompositionResult.Fail) {
-            scaffoldState.snackbarHostState.showSnackbar(
-                message = failedMessage,
-                actionLabel = okMessage,
-            )
-        }
-    }
+    val compositionResult = rememberLottieComposition(spec)
 
-    animationState.outlineMasksAndMattes = outlineMasksAndMattes.value
-    animationState.applyOpacityToLayers = applyOpacityToLayers.value
-    animationState.enableMergePaths = enableMergePaths.value
+    LaunchedEffect(compositionResult.isFailure) {
+        if (!compositionResult.isFailure) return@LaunchedEffect
+        scaffoldState.snackbarHostState.showSnackbar(
+            message = failedMessage,
+            actionLabel = okMessage,
+        )
+    }
 
     Scaffold(
         scaffoldState = scaffoldState,
-        topBar = {
-            TopAppBar(
-                title = {},
-                backgroundColor = Color.Transparent,
-                elevation = 0.dp,
-                navigationIcon = {
-                    IconButton(
-                        onClick = { backPressedDispatcher?.onBackPressed() },
-                    ) {
-                        Icon(
-                            Icons.Default.Close,
-                            contentDescription = null
-                        )
-                    }
-                },
-                actions = {
-                    if (compositionResult()?.warnings?.isNotEmpty() == true) {
-                        IconButton(
-                            onClick = { showWarningsDialog = true }
-                        ) {
-                            Icon(
-                                Icons.Filled.Warning,
-                                tint = Color.Black,
-                                contentDescription = null
-                            )
-                        }
-                    }
-                    IconButton(
-                        onClick = { focusMode = !focusMode },
-                    ) {
-                        Icon(
-                            Icons.Filled.RemoveRedEye,
-                            tint = if (focusMode) Teal else Color.Black,
-                            contentDescription = null
-                        )
-                    }
-                }
-            )
-        },
+        topBar = { PlayerPageTopAppBar(state, compositionResult.value) },
     ) {
-        Column(
-            verticalArrangement = Arrangement.SpaceBetween,
-            modifier = Modifier.fillMaxHeight()
-        ) {
-            Box(
-                contentAlignment = Alignment.Center,
-                modifier = Modifier
-                    .weight(1f)
-                    .maybeBackground(backgroundColor)
-                    .fillMaxWidth()
-            ) {
-                LottieAnimation(
-                    compositionResult,
-                    animationState = animationState,
-                    modifier = Modifier
-                        .fillMaxSize()
-                        .align(Alignment.Center)
-                        .maybeDrawBorder(borderToolbar.value)
-                )
-                if (compositionResult is LottieCompositionResult.Loading) {
-                    DebouncedCircularProgressIndicator(
-                        color = Teal,
-                        modifier = Modifier
-                            .size(48.dp)
-                    )
-                }
-            }
-            ExpandVisibility(speedToolbar.value && !focusMode) {
-                SpeedToolbar(
-                    speed = animationState.speed,
-                    onSpeedChanged = { animationState.speed = it }
-                )
-            }
-            ExpandVisibility(!focusMode && backgroundColorToolbar.value) {
-                BackgroundColorToolbar(
-                    animationBackgroundColor = animationBackgroundColor,
-                    onColorChanged = { backgroundColor = it }
-                )
-            }
-            ExpandVisibility(!focusMode) {
-                PlayerControlsRow(animationState, compositionResult())
-            }
-            ExpandVisibility(!focusMode) {
-                Toolbar(
-                    border = borderToolbar,
-                    speed = speedToolbar,
-                    backgroundColor = backgroundColorToolbar,
-                    outlineMasksAndMattes = outlineMasksAndMattes,
-                    applyOpacityToLayers = applyOpacityToLayers,
-                    enableMergePaths = enableMergePaths
-                )
-            }
-        }
+        PlayerPageContent(
+            state,
+            compositionResult.value,
+            compositionResult.isLoading,
+            animationBackgroundColor,
+        )
     }
 
-    if (showWarningsDialog) {
-        WarningDialog(warnings = compositionResult()?.warnings ?: emptyList(), onDismiss = { showWarningsDialog = false })
+    if (state.showWarningsDialog) {
+        WarningDialog(warnings = compositionResult.value?.warnings ?: emptyList(), onDismiss = { state.showWarningsDialog = false })
     }
 }
 
 @Composable
-private fun ColumnScope.ExpandVisibility(visible: Boolean, content: @Composable () -> Unit) {
+private fun ColumnScope.ExpandVisibility(
+    visible: Boolean,
+    content: @Composable () -> Unit,
+) {
     AnimatedVisibility(
         visible = visible,
         enter = expandVertically(),
@@ -200,16 +155,165 @@
 }
 
 @Composable
-private fun PlayerControlsRow(
-    animationState: LottieAnimationState,
+private fun PlayerPageTopAppBar(
+    state: PlayerPageState,
     composition: LottieComposition?,
 ) {
-    val totalTime = ((composition?.duration ?: 0L / animationState.speed) / 1000.0)
+    val backPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
+
+    TopAppBar(
+        title = {},
+        backgroundColor = Color.Transparent,
+        elevation = 0.dp,
+        navigationIcon = {
+            IconButton(
+                onClick = { backPressedDispatcher?.onBackPressed() },
+            ) {
+                Icon(
+                    Icons.Default.Close,
+                    contentDescription = null
+                )
+            }
+        },
+        actions = {
+            if (composition?.warnings?.isNotEmpty() == true) {
+                IconButton(
+                    onClick = { state.showWarningsDialog = true }
+                ) {
+                    Icon(
+                        Icons.Filled.Warning,
+                        tint = Color.Black,
+                        contentDescription = null
+                    )
+                }
+            }
+            IconButton(
+                onClick = { state.focusMode = !state.focusMode },
+            ) {
+                Icon(
+                    Icons.Filled.RemoveRedEye,
+                    tint = if (state.focusMode) Teal else Color.Black,
+                    contentDescription = null
+                )
+            }
+        }
+    )
+}
+
+@Composable
+fun PlayerPageContent(
+    state: PlayerPageState,
+    composition: LottieComposition?,
+    isLoading: Boolean,
+    animationBackgroundColor: Color?,
+) {
+    LaunchedEffect(
+        composition,
+        state.shouldPlay,
+        state.targetSpeed,
+        state.shouldLoop,
+        state.progressSliderGesture,
+    ) {
+        composition ?: return@LaunchedEffect
+        state.progressSliderGesture?.let { p ->
+            state.animatable.snapTo(composition, p, resetLastFrameNanos = true)
+            return@LaunchedEffect
+        }
+        if (state.shouldPlay) {
+            if (!state.animatable.isPlaying && state.animatable.isAtEnd) {
+                state.animatable.resetToBeginning()
+            }
+            state.animatable.animate(
+                composition,
+                iterations = if (state.shouldLoop) LottieConstants.IterateForever else 1,
+                initialProgress = state.animatable.progress,
+                speed = state.targetSpeed,
+                continueFromPreviousAnimate = state.animatable.isPlaying,
+            )
+            state.shouldPlay = false
+        }
+    }
+
+    Column(
+        verticalArrangement = Arrangement.SpaceBetween,
+        modifier = Modifier.fillMaxHeight()
+    ) {
+        Box(
+            contentAlignment = Alignment.Center,
+            modifier = Modifier
+                .weight(1f)
+                .maybeBackground(state.backgroundColor)
+                .fillMaxWidth()
+        ) {
+            PlayerPageLottieAnimation(
+                composition,
+                state.animatable.progress,
+                modifier = Modifier
+                    // TODO: figure out how maxWidth can play nice with the aspectRatio modifier inside of LottieAnimation.
+                    .fillMaxWidth()
+                    .align(Alignment.Center)
+                    .maybeDrawBorder(state.borderToolbar)
+            )
+            if (isLoading) {
+                DebouncedCircularProgressIndicator(
+                    color = Teal,
+                    modifier = Modifier
+                        .size(48.dp)
+                )
+            }
+        }
+        ExpandVisibility(state.speedToolbar && !state.focusMode) {
+            SpeedToolbar(state)
+        }
+        ExpandVisibility(!state.focusMode && state.backgroundColorToolbar) {
+            BackgroundColorToolbar(
+                animationBackgroundColor = animationBackgroundColor,
+                onColorChanged = { state.backgroundColor = it }
+            )
+        }
+        ExpandVisibility(!state.focusMode) {
+            PlayerControlsRow(state, composition)
+        }
+        ExpandVisibility(!state.focusMode) {
+            Toolbar(state)
+        }
+    }
+}
+
+@Composable
+private fun PlayerPageLottieAnimation(
+    composition: LottieComposition?,
+    progress: Float,
+    modifier: Modifier = Modifier,
+) {
+    val dummyBitmapStrokeWidth = with(LocalDensity.current) { 3.dp.toPx() }
+    val imageAssetDelegate = remember(composition) {
+        if (composition?.images?.any { (_, asset) -> asset.hasBitmap() } == true) {
+            null
+        } else {
+            ImageAssetDelegate { if (it.hasBitmap()) null else it.toDummyBitmap(dummyBitmapStrokeWidth) }
+        }
+    }
+    LottieAnimation(
+        composition,
+        progress,
+        imageAssetDelegate = imageAssetDelegate,
+        modifier = modifier,
+    )
+}
+
+@Composable
+private fun PlayerControlsRow(
+    state: PlayerPageState,
+    composition: LottieComposition?,
+) {
+    val totalTime = ((composition?.duration ?: 0L / state.animatable.speed) / 1000.0)
     val totalTimeFormatted = ("%.1f").format(totalTime)
 
-    val progress = (totalTime / 100.0) * ((animationState.progress * 100.0).roundToInt())
-    val progressFormatted = ("%.1f").format(progress)
+    val progressFormatted = ("%.1f").format(state.animatable.progress * totalTime)
 
+    val frame = composition?.getFrameForProgress(state.animatable.progress)?.roundToInt() ?: 0
+    val durationFrames = ceil(composition?.durationFrames ?: 0f).roundToInt()
     Box(
         modifier = Modifier
             .fillMaxWidth()
@@ -221,16 +325,16 @@
                 contentAlignment = Alignment.Center
             ) {
                 IconButton(
-                    onClick = { animationState.toggleIsPlaying() },
+                    onClick = { state.shouldPlay = !state.shouldPlay },
                 ) {
                     Icon(
-                        if (animationState.isPlaying) Icons.Filled.Pause
+                        if (state.animatable.isPlaying) Icons.Filled.Pause
                         else Icons.Filled.PlayArrow,
                         contentDescription = null
                     )
                 }
                 Text(
-                    "${animationState.frame}/${ceil(composition?.durationFrames ?: 0f).toInt()}\n${progressFormatted}/$totalTimeFormatted",
+                    "$frame/$durationFrames\n${progressFormatted}/$totalTimeFormatted",
                     style = TextStyle(fontSize = 8.sp),
                     textAlign = TextAlign.Center,
                     modifier = Modifier
@@ -238,18 +342,17 @@
                 )
             }
             Slider(
-                value = animationState.progress,
-                onValueChange = { animationState.progress = it },
-
+                value = state.progressSliderGesture ?: state.animatable.progress,
+                onValueChange = { state.progressSliderGesture = it },
+                onValueChangeFinished = { state.progressSliderGesture = null },
                 modifier = Modifier.weight(1f)
             )
-            IconButton(onClick = {
-                val repeatCount = if (animationState.repeatCount == Integer.MAX_VALUE) 0 else Integer.MAX_VALUE
-                animationState.repeatCount = repeatCount
-            }) {
+            IconButton(
+                onClick = { state.shouldLoop = !state.shouldLoop },
+            ) {
                 Icon(
                     Icons.Filled.Repeat,
-                    tint = if (animationState.repeatCount > 0) Teal else Color.Black,
+                    tint = if (state.animatable.iterations == 1) Color.Black else Teal,
                     contentDescription = null
                 )
             }
@@ -266,10 +369,7 @@
 }
 
 @Composable
-private fun SpeedToolbar(
-    speed: Float,
-    onSpeedChanged: (Float) -> Unit,
-) {
+private fun SpeedToolbar(state: PlayerPageState) {
     Row(
         horizontalArrangement = Arrangement.SpaceBetween,
         modifier = Modifier
@@ -279,26 +379,26 @@
     ) {
         ToolbarChip(
             label = "0.5x",
-            isActivated = speed == 0.5f,
-            onClick = { onSpeedChanged(0.5f) },
+            isActivated = state.animatable.speed == 0.5f,
+            onClick = { state.targetSpeed = 0.5f },
             modifier = Modifier.padding(end = 8.dp)
         )
         ToolbarChip(
             label = "1x",
-            isActivated = speed == 1f,
-            onClick = { onSpeedChanged(1f) },
+            isActivated = state.animatable.speed == 1f,
+            onClick = { state.targetSpeed = 1f },
             modifier = Modifier.padding(end = 8.dp)
         )
         ToolbarChip(
             label = "1.5x",
-            isActivated = speed == 1.5f,
-            onClick = { onSpeedChanged(1.5f) },
+            isActivated = state.animatable.speed == 1.5f,
+            onClick = { state.targetSpeed = 1.5f },
             modifier = Modifier.padding(end = 8.dp)
         )
         ToolbarChip(
             label = "2x",
-            isActivated = speed == 2f,
-            onClick = { onSpeedChanged(2f) },
+            isActivated = state.animatable.speed == 2f,
+            onClick = { state.targetSpeed = 2f },
             modifier = Modifier.padding(end = 8.dp)
         )
     }
@@ -352,14 +452,7 @@
 }
 
 @Composable
-private fun Toolbar(
-    border: MutableState<Boolean>,
-    speed: MutableState<Boolean>,
-    backgroundColor: MutableState<Boolean>,
-    outlineMasksAndMattes: MutableState<Boolean>,
-    applyOpacityToLayers: MutableState<Boolean>,
-    enableMergePaths: MutableState<Boolean>,
-) {
+private fun Toolbar(state: PlayerPageState) {
     Row(
         modifier = Modifier
             .horizontalScroll(rememberScrollState())
@@ -369,43 +462,43 @@
         ToolbarChip(
             iconPainter = painterResource(R.drawable.ic_masks_and_mattes),
             label = stringResource(R.string.toolbar_item_masks),
-            isActivated = outlineMasksAndMattes.value,
-            onClick = { outlineMasksAndMattes.value = it },
+            isActivated = state.outlineMasksAndMattes,
+            onClick = { state.outlineMasksAndMattes = it },
             modifier = Modifier.padding(end = 8.dp)
         )
         ToolbarChip(
             iconPainter = painterResource(R.drawable.ic_layers),
             label = stringResource(R.string.toolbar_item_opacity_layers),
-            isActivated = applyOpacityToLayers.value,
-            onClick = { applyOpacityToLayers.value = it },
+            isActivated = state.applyOpacityToLayers,
+            onClick = { state.applyOpacityToLayers = it },
             modifier = Modifier.padding(end = 8.dp)
         )
         ToolbarChip(
             iconPainter = painterResource(R.drawable.ic_color),
             label = stringResource(R.string.toolbar_item_color),
-            isActivated = backgroundColor.value,
-            onClick = { backgroundColor.value = it },
+            isActivated = state.backgroundColorToolbar,
+            onClick = { state.backgroundColorToolbar = it },
             modifier = Modifier.padding(end = 8.dp)
         )
         ToolbarChip(
             iconPainter = painterResource(R.drawable.ic_speed),
             label = stringResource(R.string.toolbar_item_speed),
-            isActivated = speed.value,
-            onClick = { speed.value = it },
+            isActivated = state.speedToolbar,
+            onClick = { state.speedToolbar = it },
             modifier = Modifier.padding(end = 8.dp)
         )
         ToolbarChip(
             iconPainter = painterResource(R.drawable.ic_border),
             label = stringResource(R.string.toolbar_item_border),
-            isActivated = border.value,
-            onClick = { border.value = it },
+            isActivated = state.borderToolbar,
+            onClick = { state.borderToolbar = it },
             modifier = Modifier.padding(end = 8.dp)
         )
         ToolbarChip(
             iconPainter = rememberVectorPainter(Icons.Default.MergeType),
             label = stringResource(R.string.toolbar_item_merge_paths),
-            isActivated = enableMergePaths.value,
-            onClick = { enableMergePaths.value = it },
+            isActivated = state.enableMergePaths,
+            onClick = { state.enableMergePaths = it },
             modifier = Modifier.padding(end = 8.dp)
         )
     }
@@ -448,11 +541,12 @@
 @Preview
 @Composable
 fun SpeedToolbarPreview() {
-    SpeedToolbar(speed = 1f, onSpeedChanged = {})
+    val state = remember { PlayerPageState(null) }
+    SpeedToolbar(state)
 }
 
 @Preview(name = "Player")
 @Composable
 fun PlayerPagePreview() {
-    PlayerPage(LottieAnimationSpec.Url("https://lottiefiles.com/download/public/32922"))
+    PlayerPage(LottieCompositionSpec.Url("https://lottiefiles.com/download/public/32922"))
 }
\ No newline at end of file
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/player/ToolbarChip.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/player/ToolbarChip.kt
index cc09630..00e5b5c 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/player/ToolbarChip.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/player/ToolbarChip.kt
@@ -1,8 +1,10 @@
 package com.airbnb.lottie.sample.compose.player
 
-import androidx.annotation.DrawableRes
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.Icon
 import androidx.compose.material.Surface
@@ -33,11 +35,11 @@
 ) {
     val unActivatedColor = remember { Color(0xFF444444) }
     Surface(
+        onClick = { onClick(!isActivated) },
         shape = RoundedCornerShape(3.dp),
         color = if (isActivated) Teal else Color(0xFFEAEAEA),
         modifier = Modifier
             .then(modifier)
-            .clickable(onClick = { onClick(!isActivated) })
             .clipToBounds()
     ) {
         Row(
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/preview/PreviewPage.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/preview/PreviewPage.kt
index 1b908d8..0c90a67 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/preview/PreviewPage.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/preview/PreviewPage.kt
@@ -3,11 +3,24 @@
 import androidx.annotation.DrawableRes
 import androidx.annotation.StringRes
 import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.*
-import androidx.compose.runtime.*
+import androidx.compose.material.Divider
+import androidx.compose.material.Icon
+import androidx.compose.material.OutlinedTextField
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.material.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -69,7 +82,7 @@
     onClick: () -> Unit,
 ) {
     Surface(
-        modifier = Modifier.clickable(onClick = onClick)
+        onClick = onClick,
     ) {
         Column {
             Row(
@@ -159,8 +172,8 @@
 @Composable
 private fun AssetRow(name: String, onClick: () -> Unit) {
     Surface(
+        onClick = onClick,
         modifier = Modifier
-            .clickable(onClick = onClick)
             .fillMaxWidth()
             .padding(vertical = 12.dp)
     ) {
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/showcase/ShowcaseViewModel.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/showcase/ShowcaseViewModel.kt
index d92c7c2..04649e9 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/showcase/ShowcaseViewModel.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/showcase/ShowcaseViewModel.kt
@@ -4,7 +4,11 @@
 import com.airbnb.lottie.sample.compose.api.LottieFilesApi
 import com.airbnb.lottie.sample.compose.dagger.AssistedViewModelFactory
 import com.airbnb.lottie.sample.compose.dagger.daggerMavericksViewModelFactory
-import com.airbnb.mvrx.*
+import com.airbnb.mvrx.Async
+import com.airbnb.mvrx.MavericksState
+import com.airbnb.mvrx.MavericksViewModel
+import com.airbnb.mvrx.MavericksViewModelFactory
+import com.airbnb.mvrx.Uninitialized
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
diff --git a/sample-compose/src/main/res/drawable/ic_docs.xml b/sample-compose/src/main/res/drawable/ic_examples.xml
similarity index 100%
rename from sample-compose/src/main/res/drawable/ic_docs.xml
rename to sample-compose/src/main/res/drawable/ic_examples.xml
diff --git a/sample-compose/src/main/res/raw/bar.json b/sample-compose/src/main/res/raw/bar.json
new file mode 100644
index 0000000..21823de
--- /dev/null
+++ b/sample-compose/src/main/res/raw/bar.json
@@ -0,0 +1 @@
+{"v":"5.7.7","fr":60,"ip":0,"op":300,"w":400,"h":25,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,12.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[400,25],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0.169,0.206],"ix":2},"a":{"a":0,"k":[-200,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[0,100]},{"t":299,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":1,"nm":"Light Gray Solid 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,12.5,0],"ix":2,"l":2},"a":{"a":0,"k":[200,12.5,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"sw":400,"sh":25,"sc":"#afafaf","ip":0,"op":300,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/sample-compose/src/main/res/raw/bar_1.json b/sample-compose/src/main/res/raw/bar_1.json
new file mode 100644
index 0000000..f680611
--- /dev/null
+++ b/sample-compose/src/main/res/raw/bar_1.json
@@ -0,0 +1 @@
+{"v":"5.7.7","fr":60,"ip":0,"op":91,"w":400,"h":25,"nm":"Bar 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,12.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[400,25],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0.169,0.206],"ix":2},"a":{"a":0,"k":[-200,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[0,100]},{"t":299,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":91,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":1,"nm":"Light Gray Solid 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,12.5,0],"ix":2,"l":2},"a":{"a":0,"k":[200,12.5,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"sw":400,"sh":25,"sc":"#afafaf","ip":0,"op":91,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/sample-compose/src/main/res/raw/bar_2.json b/sample-compose/src/main/res/raw/bar_2.json
new file mode 100644
index 0000000..aabb8ab
--- /dev/null
+++ b/sample-compose/src/main/res/raw/bar_2.json
@@ -0,0 +1 @@
+{"v":"5.7.7","fr":60,"ip":0,"op":110,"w":400,"h":25,"nm":"Bar 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,12.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[400,25],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0.169,0.206],"ix":2},"a":{"a":0,"k":[-200,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-90,"s":[0,100]},{"t":209,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":110,"st":-90,"bm":0},{"ddd":0,"ind":2,"ty":1,"nm":"Light Gray Solid 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,12.5,0],"ix":2,"l":2},"a":{"a":0,"k":[200,12.5,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"sw":400,"sh":25,"sc":"#afafaf","ip":0,"op":110,"st":-90,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/sample-compose/src/main/res/raw/bar_3.json b/sample-compose/src/main/res/raw/bar_3.json
new file mode 100644
index 0000000..51aa393
--- /dev/null
+++ b/sample-compose/src/main/res/raw/bar_3.json
@@ -0,0 +1 @@
+{"v":"5.7.7","fr":60,"ip":0,"op":101,"w":400,"h":25,"nm":"Bar 3","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,12.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[400,25],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0.169,0.206],"ix":2},"a":{"a":0,"k":[-200,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-199,"s":[0,100]},{"t":100,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":101,"st":-199,"bm":0},{"ddd":0,"ind":2,"ty":1,"nm":"Light Gray Solid 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,12.5,0],"ix":2,"l":2},"a":{"a":0,"k":[200,12.5,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"sw":400,"sh":25,"sc":"#afafaf","ip":0,"op":101,"st":-199,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/sample-compose/src/main/res/raw/heart.json b/sample-compose/src/main/res/raw/heart.json
new file mode 100755
index 0000000..48fa0ec
--- /dev/null
+++ b/sample-compose/src/main/res/raw/heart.json
@@ -0,0 +1 @@
+{"v":"4.11.1","fr":60,"ip":0,"op":116,"w":50,"h":50,"nm":"TwitterHeart","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Dot14","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-320,"ix":10},"p":{"a":0,"k":[25,25,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[40,40,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-37.5,-40.5],[-1,0.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[47],"e":[29]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[29],"e":[9]},{"t":78}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[48],"e":[30]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[30],"e":[10]},{"t":78}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[0.631372570992,0.811764717102,0.937254905701,1],"e":[0.819607853889,0.647058844566,0.909803926945,1]},{"t":56}],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[5],"e":[2]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[2],"e":[0]},{"t":70}],"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":44,"op":90,"st":-44,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Dot13","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-306.6,"ix":10},"p":{"a":0,"k":[25,25,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[40,40,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-37.5,-40.5],[-1,0.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[44],"e":[19]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[19],"e":[0]},{"t":89}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[45],"e":[20]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[20],"e":[1]},{"t":89}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[0.658823549747,0.800000011921,0.96862745285,1],"e":[0.815686285496,0.654901981354,0.905882358551,1]},{"t":56}],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[5],"e":[0]},{"t":89}],"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":44,"op":90,"st":-44,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Dot12","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-271.7,"ix":10},"p":{"a":0,"k":[25,25,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[40,40,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-37.5,-40.5],[-1,0.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[47],"e":[29]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[29],"e":[9]},{"t":78}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[48],"e":[30]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[30],"e":[10]},{"t":78}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[0.784313738346,0.588235318661,0.901960790157,1],"e":[0.890196084976,0.819607853889,0.580392181873,1]},{"t":56}],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[5],"e":[2]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[2],"e":[0]},{"t":70}],"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":44,"op":90,"st":-44,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Dot11","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-258.3,"ix":10},"p":{"a":0,"k":[25,25,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[40,40,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-37.5,-40.5],[-1,0.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[44],"e":[19]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[19],"e":[0]},{"t":89}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[45],"e":[20]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[20],"e":[1]},{"t":89}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[0.784313738346,0.61960786581,0.89411765337,1],"e":[0.921568632126,0.749019622803,0.32549020648,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[0.921568632126,0.749019622803,0.32549020648,1],"e":[0.549019634724,0.274509817362,0.709803938866,1]},{"t":66}],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[5],"e":[0]},{"t":89}],"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":44,"op":90,"st":-44,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Dot10","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-220.3,"ix":10},"p":{"a":0,"k":[25,25,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[40,40,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-37.5,-40.5],[-1,0.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[47],"e":[29]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[29],"e":[9]},{"t":78}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[48],"e":[30]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[30],"e":[10]},{"t":78}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[0.607843160629,0.886274516582,0.78823530674,1],"e":[0.792156875134,0.737254917622,0.600000023842,1]},{"t":56}],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[5],"e":[2]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[2],"e":[0]},{"t":70}],"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":44,"op":90,"st":-44,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Dot9","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-206.9,"ix":10},"p":{"a":0,"k":[25,25,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[40,40,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-37.5,-40.5],[-1,0.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[44],"e":[19]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[19],"e":[0]},{"t":89}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[45],"e":[20]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[20],"e":[1]},{"t":89}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[0.61960786581,0.882352948189,0.780392169952,1],"e":[0.792156875134,0.737254917622,0.600000023842,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[0.792156875134,0.737254917622,0.600000023842,1],"e":[0.549019634724,0.274509817362,0.709803938866,1]},{"t":66}],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[5],"e":[0]},{"t":89}],"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tm","s":{"a":0,"k":44,"ix":1},"e":{"a":0,"k":45,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":4,"nm":"Trim Paths 2","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":44,"op":90,"st":-44,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Dot8","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-168.2,"ix":10},"p":{"a":0,"k":[25,25,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[40,40,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-37.5,-40.5],[-1,0.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[47],"e":[29]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[29],"e":[9]},{"t":78}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[48],"e":[30]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[30],"e":[10]},{"t":78}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[0.858823537827,0.623529434204,0.68235296011,1],"e":[0.329411774874,0.596078455448,0.800000011921,1]},{"t":56}],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[5],"e":[2]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[2],"e":[0]},{"t":70}],"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":44,"op":90,"st":-44,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Dot7","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-154.8,"ix":10},"p":{"a":0,"k":[25,25,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[40,40,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-37.5,-40.5],[-1,0.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[44],"e":[19]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[19],"e":[0]},{"t":89}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[45],"e":[20]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[20],"e":[1]},{"t":89}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[0.890196084976,0.600000023842,0.694117665291,1],"e":[0.329411774874,0.596078455448,0.800000011921,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[0.329411774874,0.596078455448,0.800000011921,1],"e":[0.549019634724,0.274509817362,0.709803938866,1]},{"t":66}],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[5],"e":[0]},{"t":89}],"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":44,"op":90,"st":-44,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Dot6","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-117.1,"ix":10},"p":{"a":0,"k":[25,25,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[40,40,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-37.5,-40.5],[-1,0.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[47],"e":[29]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[29],"e":[9]},{"t":78}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[48],"e":[30]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[30],"e":[10]},{"t":78}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[0.61960786581,0.815686285496,0.956862747669,1],"e":[0.701960802078,0.843137264252,0.658823549747,1]},{"t":56}],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[5],"e":[2]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[2],"e":[0]},{"t":70}],"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tm","s":{"a":0,"k":29,"ix":1},"e":{"a":0,"k":30,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":4,"nm":"Trim Paths 2","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":44,"op":90,"st":-44,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Dot5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-103.7,"ix":10},"p":{"a":0,"k":[25,25,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[40,40,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-37.5,-40.5],[-1,0.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[44],"e":[19]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[19],"e":[0]},{"t":89}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[45],"e":[20]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[20],"e":[1]},{"t":89}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[0.721568644047,0.847058832645,0.949019610882,1],"e":[0.701960802078,0.843137264252,0.670588254929,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[0.701960802078,0.843137264252,0.670588254929,1],"e":[0.549019634724,0.274509817362,0.709803938866,1]},{"t":66}],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[5],"e":[0]},{"t":89}],"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":44,"op":90,"st":-44,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Dot4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-69.3,"ix":10},"p":{"a":0,"k":[25,25,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[40,40,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-37.5,-40.5],[-1,0.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[47],"e":[29]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[29],"e":[9]},{"t":78}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[48],"e":[30]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[30],"e":[10]},{"t":78}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[0.800000011921,0.580392181873,0.929411768913,1],"e":[0.701960802078,0.847058832645,0.658823549747,1]},{"t":56}],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[5],"e":[2]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[2],"e":[0]},{"t":70}],"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":44,"op":90,"st":-44,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Dot3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-55.9,"ix":10},"p":{"a":0,"k":[25,25,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[40,40,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-37.5,-40.5],[-1,0.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[44],"e":[19]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[19],"e":[0]},{"t":89}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[45],"e":[20]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[20],"e":[1]},{"t":89}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[0.643137276173,0.811764717102,0.972549021244,1],"e":[0.701960802078,0.847058832645,0.658823549747,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[0.701960802078,0.847058832645,0.658823549747,1],"e":[0.549019634724,0.274509817362,0.709803938866,1]},{"t":66}],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[5],"e":[0]},{"t":89}],"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":44,"op":90,"st":-44,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"Dot2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-13.4,"ix":10},"p":{"a":0,"k":[25,25,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[40,40,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-37.5,-40.5],[-1,0.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[47],"e":[29]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[29],"e":[9]},{"t":78}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[48],"e":[30]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[30],"e":[10]},{"t":78}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[0.607843160629,0.886274516582,0.78823530674,1],"e":[0.631372570992,0.509803950787,0.623529434204,1]},{"t":56}],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[5],"e":[2]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[2],"e":[0]},{"t":70}],"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":44,"op":90,"st":-44,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"Dot1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25,25,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[40,40,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-37.5,-40.5],[-1,0.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[44],"e":[19]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[19],"e":[0]},{"t":89}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[45],"e":[20]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[20],"e":[1]},{"t":89}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":44,"s":[0.61960786581,0.882352948189,0.780392169952,1],"e":[0.800000011921,0.521568655968,0.760784327984,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[0.800000011921,0.521568655968,0.760784327984,1],"e":[0.549019634724,0.274509817362,0.709803938866,1]},{"t":66}],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":56,"s":[5],"e":[0]},{"t":89}],"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":44,"op":90,"st":-44,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"C2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25,25,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[40,40,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":38,"s":[25.744,25.744],"e":[60.744,60.744]},{"t":45}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.807843148708,0.57647061348,0.956862747669,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":38,"s":[23.3],"e":[1]},{"t":45}],"ix":5},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":38,"op":46,"st":-47,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"C1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25,25,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":33,"s":[4,4,100],"e":[40,40,100]},{"t":39}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[57.344,57.344],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":33,"s":[1,0,0.257425785065,1],"e":[0.811764717102,0.564705908298,0.96862745285,1]},{"t":39}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":33,"op":39,"st":-46,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"H2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.217,25.85,0],"ix":2},"a":{"a":0,"k":[2.958,2.958,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.32,0.32,0.32],"y":[1,1,1]},"o":{"x":[0.68,0.68,0.68],"y":[0,0,0]},"n":["0p32_1_0p68_0","0p32_1_0p68_0","0p32_1_0p68_0"],"t":43,"s":[4,4,100],"e":[48.44,48.44,100]},{"i":{"x":[0.32,0.32,0.32],"y":[1,1,1]},"o":{"x":[0.68,0.68,0.68],"y":[0,0,0]},"n":["0p32_1_0p68_0","0p32_1_0p68_0","0p32_1_0p68_0"],"t":54,"s":[48.44,48.44,100],"e":[37.04,37.04,100]},{"i":{"x":[0.32,0.32,0.32],"y":[1,1,1]},"o":{"x":[0.68,0.68,0.68],"y":[0,0,0]},"n":["0p32_1_0p68_0","0p32_1_0p68_0","0p32_1_0p68_0"],"t":70,"s":[37.04,37.04,100],"e":[40,40,100]},{"t":91}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.833,0],[0,-3.333],[-3.25,0],[0,8.333],[3.917,0],[0,0]],"o":[[-4.833,0],[0,7.667],[3.25,0],[0,-4.5],[-3.917,0],[0,0]],"v":[[-4.583,-10.167],[-11.25,-2.25],[2.833,16.083],[17.167,-2.333],[10.167,-10],[2.917,-5.917]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.835294127464,0.180392161012,0.321568638086,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":43,"op":136,"st":-46,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"H1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.217,25.85,0],"ix":2},"a":{"a":0,"k":[2.958,2.958,0],"ix":1},"s":{"a":0,"k":[40,40,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.833,0],[0,-3.333],[-3.25,0],[0,8.333],[3.917,0],[0,0]],"o":[[-4.833,0],[0,7.667],[3.25,0],[0,-4.5],[-3.917,0],[0,0]],"v":[[-4.583,-10.167],[-11.25,-2.25],[2.833,16.083],[17.167,-2.333],[10.167,-10],[2.917,-5.917]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.674509823322,0.729411780834,0.764705896378,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":33,"st":-46,"bm":0}]}
\ No newline at end of file
diff --git a/sample-compose/src/main/res/raw/walkthrough.json b/sample-compose/src/main/res/raw/walkthrough.json
new file mode 100644
index 0000000..a41a24e
--- /dev/null
+++ b/sample-compose/src/main/res/raw/walkthrough.json
@@ -0,0 +1 @@
+{"assets":[],"layers":[{"ddd":0,"ind":0,"ty":1,"nm":"MASTER","ks":{"o":{"k":0},"r":{"k":0},"p":{"k":[{"i":{"x":0.119,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p119_1_0p167_0p167","t":120,"s":[381.957,254.322,0],"e":[203.957,254.322,0],"to":[-29.6666660308838,0,0],"ti":[29.6666660308838,0,0]},{"t":180}]},"a":{"k":[60,60,0]},"s":{"k":[61.8,61.8,100]}},"ao":0,"sw":120,"sh":120,"sc":"#ffffff","ip":117,"op":181,"st":105,"bm":0,"sr":1},{"ddd":0,"ind":1,"ty":4,"nm":"L-B","parent":0,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[39.043,45.678,0]},"a":{"k":[250,250,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":{"i":[[0,0],[25.671,-4.167],[1.456,6.902],[-8.481,1.863],[-47.562,13.01],[-0.501,0.133],[-71.423,-2.315]],"o":[[0,0],[-8.224,1.335],[-1.456,-6.903],[23.817,-5.233],[0.16,-0.044],[0.501,-0.133],[0,0]],"v":[[-8.837,-58.229],[-35.834,33.662],[-51.688,23.148],[-41.174,7.293],[51.797,44.178],[53.188,43.741],[140.394,43.672]],"c":false}},"nm":"Path 1"},{"ty":"st","fillEnabled":true,"c":{"k":[0,0.48,0.53,1]},"o":{"k":100},"w":{"k":9.194},"lc":2,"lj":1,"ml":10,"nm":"Stroke 1"},{"ty":"tr","p":{"k":[166.029,270.643],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 8"},{"ty":"tm","s":{"k":[{"i":{"x":[0.703],"y":[0.821]},"o":{"x":[0.167],"y":[0.167]},"n":["0p703_0p821_0p167_0p167"],"t":123,"s":[80],"e":[50]},{"i":{"x":[0.263],"y":[1]},"o":{"x":[0.037],"y":[0.168]},"n":["0p263_1_0p037_0p168"],"t":128,"s":[50],"e":[30]},{"t":160}],"ix":1},"e":{"k":[{"i":{"x":[0.337],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p337_1_0p167_0p167"],"t":123,"s":[81],"e":[73.4]},{"t":134}],"ix":2},"o":{"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1"}],"ip":123,"op":181,"st":113,"bm":0,"sr":1},{"ddd":0,"ind":2,"ty":4,"nm":"L-Y","parent":0,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[39.043,45.678,0]},"a":{"k":[250,250,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":{"i":[[0,0],[25.671,-4.167],[1.456,6.902],[-8.481,1.863],[-47.562,13.01],[-0.501,0.133],[-71.423,-2.315]],"o":[[0,0],[-8.224,1.335],[-1.456,-6.903],[23.817,-5.233],[0.16,-0.044],[0.501,-0.133],[0,0]],"v":[[-8.837,-58.229],[-35.834,33.662],[-51.688,23.148],[-41.174,7.293],[51.797,44.178],[53.188,43.741],[140.394,43.672]],"c":false}},"nm":"Path 1"},{"ty":"st","fillEnabled":true,"c":{"k":[0,0.82,0.76,1]},"o":{"k":100},"w":{"k":8.4},"lc":2,"lj":1,"ml":10,"nm":"Stroke 1"},{"ty":"tr","p":{"k":[166.029,270.643],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 8"},{"ty":"tm","s":{"k":[{"i":{"x":[0.703],"y":[0.857]},"o":{"x":[0.167],"y":[0.167]},"n":["0p703_0p857_0p167_0p167"],"t":121,"s":[80],"e":[50]},{"i":{"x":[0.938],"y":[1]},"o":{"x":[0.333],"y":[0.202]},"n":["0p938_1_0p333_0p202"],"t":125,"s":[50],"e":[0]},{"t":133}],"ix":1},"e":{"k":[{"i":{"x":[0.337],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p337_1_0p167_0p167"],"t":121,"s":[81],"e":[73.4]},{"t":132}],"ix":2},"o":{"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1"}],"ip":121,"op":181,"st":113,"bm":0,"sr":1},{"ddd":0,"ind":3,"ty":1,"nm":"N","parent":0,"ks":{"o":{"k":0},"r":{"k":0},"p":{"k":[{"i":{"x":0.26,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p26_1_0p167_0p167","t":132,"s":[-33.667,8.182,0],"e":[-33.667,-72.818,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.74,"y":0},"n":"0p833_0p833_0p74_0","t":144,"s":[-33.667,-72.818,0],"e":[-33.667,102.057,0],"to":[0,0,0],"ti":[0,0,0]},{"t":158}]},"a":{"k":[60,60,0]},"s":{"k":[100,100,100]}},"ao":0,"sw":120,"sh":120,"sc":"#ffffff","ip":132,"op":158,"st":104,"bm":0,"sr":1},{"ddd":0,"ind":4,"ty":4,"nm":"T1a-B","parent":14,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[250,250,0]},"a":{"k":[250,250,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":{"i":[[-0.5,9.501],[-0.048,5.655],[0.054,0.06],[0.946,1.486],[-9.967,8.05],[-40.546,0]],"o":[[0.031,-0.594],[0.076,-8.978],[-1.161,-1.3],[-5.939,-9.327],[24.677,-19.929],[0,0]],"v":[[-30.72,63.761],[-30.741,45.192],[-37.397,27.014],[-40.698,22.661],[-37.873,-7.117],[49.506,11.559]],"c":false}},"nm":"Path 1"},{"ty":"tm","s":{"k":24.9,"ix":1},"e":{"k":[{"i":{"x":[0.673],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p673_1_0p167_0p167"],"t":139,"s":[24.9],"e":[89.1]},{"t":153}],"ix":2},"o":{"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1"},{"ty":"st","fillEnabled":true,"c":{"k":[0,0.82,0.76,1]},"o":{"k":100},"w":{"k":9.194},"lc":2,"lj":1,"ml":10,"nm":"Stroke 1"},{"ty":"tr","p":{"k":[227.677,234.375],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 9"}],"ip":139,"op":181,"st":86,"bm":0,"sr":1},{"ddd":0,"ind":5,"ty":4,"nm":"T2a-B","parent":0,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[39.043,45.678,0]},"a":{"k":[250,250,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1.681,-29.992],[-1.681,29.992]],"c":false}},"nm":"Path 1"},{"ty":"tm","s":{"k":[{"i":{"x":[0.06],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p06_1_0p167_0p167"],"t":144,"s":[50],"e":[0]},{"t":154}],"ix":1},"e":{"k":[{"i":{"x":[0.06],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p06_1_0p167_0p167"],"t":144,"s":[50],"e":[100]},{"t":154}],"ix":2},"o":{"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1"},{"ty":"st","fillEnabled":true,"c":{"k":[0,0.48,0.53,1]},"o":{"k":100},"w":{"k":9.194},"lc":3,"lj":1,"ml":10,"nm":"Stroke 1"},{"ty":"tr","p":{"k":[277.698,247.258],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 7"}],"ip":144,"op":181,"st":84,"bm":0,"sr":1},{"ddd":0,"ind":6,"ty":4,"nm":"T1a-Y 2","parent":0,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p667_1_0p167_0p167","t":125,"s":[39.043,48.678,0],"e":[39.043,45.678,0],"to":[0,0,0],"ti":[0,0,0]},{"t":133}]},"a":{"k":[250,250,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":{"i":[[-0.5,9.501],[-0.048,5.655],[0.054,0.06],[0.946,1.486],[-9.967,8.05],[-40.546,0]],"o":[[0.031,-0.594],[0.076,-8.978],[-1.161,-1.3],[-5.939,-9.327],[24.677,-19.929],[0,0]],"v":[[-30.72,63.761],[-30.741,45.192],[-37.397,27.014],[-40.698,22.661],[-37.873,-7.117],[49.506,11.559]],"c":false}},"nm":"Path 1"},{"ty":"tm","s":{"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.301],"y":[0]},"n":["0p833_1_0p301_0"],"t":123,"s":[0],"e":[24.9]},{"t":139}],"ix":1},"e":{"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.301],"y":[0]},"n":["0p667_1_0p301_0"],"t":123,"s":[0],"e":[100]},{"t":147}],"ix":2},"o":{"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1"},{"ty":"st","fillEnabled":true,"c":{"k":[0,0.48,0.53,1]},"o":{"k":100},"w":{"k":8.4},"lc":2,"lj":1,"ml":10,"nm":"Stroke 1"},{"ty":"tr","p":{"k":[227.677,234.375],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 9"}],"ip":128,"op":181,"st":81,"bm":0,"sr":1},{"ddd":0,"ind":7,"ty":4,"nm":"O-B","parent":0,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":100,"s":[-62.792,73.057,0],"e":[-53.792,7.557,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.638,"y":1},"o":{"x":0.167,"y":0.198},"n":"0p638_1_0p167_0p198","t":104.257,"s":[-53.792,7.557,0],"e":[-33.667,-72.818,0],"to":[0,0,0],"ti":[-19.1562919616699,1.73831975460052,0]},{"i":{"x":0.795,"y":1},"o":{"x":0.523,"y":0},"n":"0p795_1_0p523_0","t":113,"s":[-33.667,-72.818,0],"e":[-14.167,102.182,0],"to":[16.2075271606445,-1.47073686122894,0],"ti":[0,0,0]},{"i":{"x":0.348,"y":1},"o":{"x":0.18,"y":0},"n":"0p348_1_0p18_0","t":123,"s":[-14.167,102.182,0],"e":[-14.167,59.182,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.27,"y":1},"o":{"x":0.693,"y":0},"n":"0p27_1_0p693_0","t":132,"s":[-14.167,59.182,0],"e":[-14.167,62.182,0],"to":[0,0,0],"ti":[0,0,0]},{"t":142}]},"a":{"k":[196.791,266.504,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":123,"s":[3,3],"e":[44.6,44.6]},{"t":130}]},"p":{"k":[0.8,-0.5]},"nm":"Ellipse Path 1"},{"ty":"st","fillEnabled":true,"c":{"k":[0,0.48,0.53,1]},"o":{"k":100},"w":{"k":9.194},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1"},{"ty":"tr","p":{"k":[196,267],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1"},{"ty":"tm","s":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":123,"s":[0],"e":[30]},{"i":{"x":[0.432],"y":[1]},"o":{"x":[0.167],"y":[1.124]},"n":["0p432_1_0p167_1p124"],"t":132,"s":[30],"e":[39.9]},{"t":160}],"ix":1},"e":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":123,"s":[100],"e":[88]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":132,"s":[88],"e":[88]},{"t":160}],"ix":2},"o":{"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1"}],"ip":123,"op":181,"st":73,"bm":0,"sr":1},{"ddd":0,"ind":8,"ty":4,"nm":"O-Y","parent":0,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":100,"s":[-62.792,73.057,0],"e":[-53.792,7.557,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.638,"y":1},"o":{"x":0.167,"y":0.198},"n":"0p638_1_0p167_0p198","t":104.257,"s":[-53.792,7.557,0],"e":[-33.667,-72.818,0],"to":[0,0,0],"ti":[-19.1562919616699,1.73831975460052,0]},{"i":{"x":0.795,"y":1},"o":{"x":0.523,"y":0},"n":"0p795_1_0p523_0","t":113,"s":[-33.667,-72.818,0],"e":[-14.167,102.182,0],"to":[16.2075271606445,-1.47073686122894,0],"ti":[0,0,0]},{"i":{"x":0.348,"y":1},"o":{"x":0.18,"y":0},"n":"0p348_1_0p18_0","t":123,"s":[-14.167,102.182,0],"e":[-14.167,59.182,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.27,"y":1},"o":{"x":0.693,"y":0},"n":"0p27_1_0p693_0","t":132,"s":[-14.167,59.182,0],"e":[-14.167,62.182,0],"to":[0,0,0],"ti":[0,0,0]},{"t":142}]},"a":{"k":[196.791,266.504,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":123,"s":[3,3],"e":[44.6,44.6]},{"t":130}]},"p":{"k":[0.8,-0.5]},"nm":"Ellipse Path 1"},{"ty":"st","fillEnabled":true,"c":{"k":[0,0.82,0.76,1]},"o":{"k":100},"w":{"k":8.8},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1"},{"ty":"tr","p":{"k":[196,267],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1"}],"ip":123,"op":181,"st":73,"bm":0,"sr":1},{"ddd":0,"ind":9,"ty":4,"nm":"T1b-B","parent":0,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[39.043,45.678,0]},"a":{"k":[250,250,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1.768,-25.966],[-1.768,25.966]],"c":false}},"nm":"Path 1"},{"ty":"tm","s":{"k":0,"ix":1},"e":{"k":[{"i":{"x":[0.21],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p21_1_0p167_0p167"],"t":150,"s":[11.7],"e":[100]},{"t":157}],"ix":2},"o":{"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1"},{"ty":"st","fillEnabled":true,"c":{"k":[0,0.82,0.76,1]},"o":{"k":100},"w":{"k":9.194},"lc":2,"lj":2,"nm":"Stroke 1"},{"ty":"tr","p":{"k":[242.756,265.581],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 10"}],"ip":150,"op":181,"st":95,"bm":0,"sr":1},{"ddd":0,"ind":10,"ty":4,"nm":"T1b-Y","parent":0,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[39.043,45.678,0]},"a":{"k":[250,250,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1.768,-25.966],[-1.768,25.966]],"c":false}},"nm":"Path 1"},{"ty":"tm","s":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":139,"s":[0],"e":[0]},{"t":144}],"ix":1},"e":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":139,"s":[11.7],"e":[100]},{"t":144}],"ix":2},"o":{"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1"},{"ty":"st","fillEnabled":true,"c":{"k":[0,0.48,0.53,1]},"o":{"k":100},"w":{"k":8.4},"lc":2,"lj":2,"nm":"Stroke 1"},{"ty":"tr","p":{"k":[242.756,265.581],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 10"}],"ip":139,"op":181,"st":84,"bm":0,"sr":1},{"ddd":0,"ind":11,"ty":4,"nm":"T2b-B","parent":0,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[39.043,45.678,0]},"a":{"k":[250,250,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[246.65,213.814],[340.956,213.628]],"c":false}},"nm":"Path 1"},{"ty":"tm","s":{"k":[{"i":{"x":[0.12],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p12_1_0p167_0p167"],"t":151,"s":[29],"e":[0]},{"t":160}],"ix":1},"e":{"k":[{"i":{"x":[0.12],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p12_1_0p167_0p167"],"t":151,"s":[41.1],"e":[66.5]},{"t":160}],"ix":2},"o":{"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1"},{"ty":"st","fillEnabled":true,"c":{"k":[0,0.48,0.53,1]},"o":{"k":100},"w":{"k":9.194},"lc":2,"lj":1,"ml":10,"nm":"Stroke 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5"}],"ip":151,"op":181,"st":52,"bm":0,"sr":1},{"ddd":0,"ind":12,"ty":4,"nm":"T2a-Y","parent":0,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[39.043,45.678,0]},"a":{"k":[250,250,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1.681,-29.992],[-1.681,29.992]],"c":false}},"nm":"Path 1"},{"ty":"tm","s":{"k":[{"i":{"x":[0.06],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p06_1_0p167_0p167"],"t":141,"s":[50],"e":[0]},{"t":151}],"ix":1},"e":{"k":[{"i":{"x":[0.06],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p06_1_0p167_0p167"],"t":141,"s":[50],"e":[100]},{"t":151}],"ix":2},"o":{"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1"},{"ty":"st","fillEnabled":true,"c":{"k":[0,0.82,0.76,1]},"o":{"k":100},"w":{"k":9.194},"lc":3,"lj":1,"ml":10,"nm":"Stroke 1"},{"ty":"tr","p":{"k":[277.698,247.258],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 7"}],"ip":141,"op":158,"st":81,"bm":0,"sr":1},{"ddd":0,"ind":13,"ty":4,"nm":"T2b-Y","parent":0,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[39.043,45.678,0]},"a":{"k":[250,250,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[246.65,213.814],[340.956,213.628]],"c":false}},"nm":"Path 1"},{"ty":"tm","s":{"k":[{"i":{"x":[0.12],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p12_1_0p167_0p167"],"t":145,"s":[29],"e":[0]},{"t":154}],"ix":1},"e":{"k":[{"i":{"x":[0.12],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p12_1_0p167_0p167"],"t":145,"s":[41.1],"e":[66.5]},{"t":154}],"ix":2},"o":{"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1"},{"ty":"st","fillEnabled":true,"c":{"k":[0,0.82,0.76,1]},"o":{"k":100},"w":{"k":9.194},"lc":2,"lj":1,"ml":10,"nm":"Stroke 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5"}],"ip":145,"op":161,"st":46,"bm":0,"sr":1},{"ddd":0,"ind":14,"ty":4,"nm":"T1a-Y","parent":0,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p667_1_0p167_0p167","t":125,"s":[39.043,48.678,0],"e":[39.043,45.678,0],"to":[0,0,0],"ti":[0,0,0]},{"t":133}]},"a":{"k":[250,250,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":{"i":[[-0.5,9.501],[-0.048,5.655],[0.054,0.06],[0.946,1.486],[-9.967,8.05],[-40.546,0]],"o":[[0.031,-0.594],[0.076,-8.978],[-1.161,-1.3],[-5.939,-9.327],[24.677,-19.929],[0,0]],"v":[[-30.72,63.761],[-30.741,45.192],[-37.397,27.014],[-40.698,22.661],[-37.873,-7.117],[49.506,11.559]],"c":false}},"nm":"Path 1"},{"ty":"tm","s":{"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.301],"y":[0]},"n":["0p833_1_0p301_0"],"t":123,"s":[0],"e":[24.9]},{"t":139}],"ix":1},"e":{"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.301],"y":[0]},"n":["0p667_1_0p301_0"],"t":123,"s":[0],"e":[100]},{"t":143}],"ix":2},"o":{"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1"},{"ty":"st","fillEnabled":true,"c":{"k":[0,0.82,0.76,1]},"o":{"k":100},"w":{"k":8.4},"lc":2,"lj":1,"ml":10,"nm":"Stroke 1"},{"ty":"tr","p":{"k":[227.677,234.375],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 9"}],"ip":128,"op":181,"st":81,"bm":0,"sr":1},{"ddd":0,"ind":15,"ty":4,"nm":"E1-B","parent":16,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[344.672,214.842,0]},"a":{"k":[344.672,214.842,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-13.664,-0.145],[62.163,0.29]],"c":false}},"nm":"Path 1"},{"ty":"st","fillEnabled":true,"c":{"k":[0,0.82,0.76,1]},"o":{"k":100},"w":{"k":9.562},"lc":2,"lj":1,"ml":10,"nm":"Stroke 1"},{"ty":"tr","p":{"k":[344.672,214.842],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2"},{"ty":"tm","s":{"k":[{"i":{"x":[0.12],"y":[0.12]},"o":{"x":[0.167],"y":[0.167]},"n":["0p12_0p12_0p167_0p167"],"t":153,"s":[0],"e":[0]},{"t":162}],"ix":1},"e":{"k":[{"i":{"x":[0.12],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p12_1_0p167_0p167"],"t":153,"s":[0],"e":[37.5]},{"t":162}],"ix":2},"o":{"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1"}],"ip":153,"op":181,"st":153,"bm":0,"sr":1},{"ddd":0,"ind":16,"ty":4,"nm":"E1-Y","parent":0,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[{"i":{"x":0.12,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p12_1_0p167_0p167","t":148,"s":[113.715,9.146,0],"e":[137.715,9.146,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.12,"y":1},"o":{"x":0.167,"y":0},"n":"0p12_1_0p167_0","t":157,"s":[137.715,9.146,0],"e":[133.715,9.146,0],"to":[0,0,0],"ti":[0,0,0]},{"t":161}]},"a":{"k":[344.672,214.842,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-13.664,-0.145],[62.163,0.29]],"c":false}},"nm":"Path 1"},{"ty":"st","fillEnabled":true,"c":{"k":[0,0.48,0.53,1]},"o":{"k":100},"w":{"k":8.4},"lc":2,"lj":1,"ml":10,"nm":"Stroke 1"},{"ty":"tr","p":{"k":[344.672,214.842],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2"},{"ty":"tm","s":{"k":[{"i":{"x":[0.12],"y":[0.12]},"o":{"x":[0.167],"y":[0.167]},"n":["0p12_0p12_0p167_0p167"],"t":148,"s":[0],"e":[0]},{"t":157}],"ix":1},"e":{"k":[{"i":{"x":[0.12],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p12_1_0p167_0p167"],"t":148,"s":[0],"e":[37.5]},{"t":157}],"ix":2},"o":{"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1"}],"ip":148,"op":163,"st":148,"bm":0,"sr":1},{"ddd":0,"ind":17,"ty":4,"nm":"E2-B","parent":18,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[332.05,237.932,0]},"a":{"k":[332.05,237.932,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-26.67,-0.283],[99.171,0.066]],"c":false}},"nm":"Path 1"},{"ty":"tm","s":{"k":[{"i":{"x":[0.12],"y":[0.12]},"o":{"x":[0.167],"y":[0.167]},"n":["0p12_0p12_0p167_0p167"],"t":155,"s":[0],"e":[0]},{"t":164}],"ix":1},"e":{"k":[{"i":{"x":[0.12],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p12_1_0p167_0p167"],"t":155,"s":[0],"e":[43]},{"t":164}],"ix":2},"o":{"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1"},{"ty":"st","fillEnabled":true,"c":{"k":[0,0.82,0.76,1]},"o":{"k":100},"w":{"k":9.562},"lc":2,"lj":1,"ml":10,"nm":"Stroke 1"},{"ty":"tr","p":{"k":[331.664,238.14],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3"}],"ip":155,"op":181,"st":155,"bm":0,"sr":1},{"ddd":0,"ind":18,"ty":4,"nm":"E2-Y","parent":0,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[{"i":{"x":0.12,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p12_1_0p167_0p167","t":152,"s":[109.092,33.61,0],"e":[121.092,33.61,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.12,"y":0.12},"o":{"x":0.167,"y":0.167},"n":"0p12_0p12_0p167_0p167","t":161,"s":[121.092,33.61,0],"e":[121.092,33.61,0],"to":[0,0,0],"ti":[0,0,0]},{"t":165}]},"a":{"k":[332.05,237.932,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-26.67,-0.283],[99.171,0.066]],"c":false}},"nm":"Path 1"},{"ty":"tm","s":{"k":[{"i":{"x":[0.12],"y":[0.12]},"o":{"x":[0.167],"y":[0.167]},"n":["0p12_0p12_0p167_0p167"],"t":152,"s":[0],"e":[0]},{"t":161}],"ix":1},"e":{"k":[{"i":{"x":[0.12],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p12_1_0p167_0p167"],"t":152,"s":[0],"e":[43]},{"t":161}],"ix":2},"o":{"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1"},{"ty":"st","fillEnabled":true,"c":{"k":[0,0.48,0.53,1]},"o":{"k":100},"w":{"k":8.4},"lc":2,"lj":1,"ml":10,"nm":"Stroke 1"},{"ty":"tr","p":{"k":[331.664,238.14],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3"}],"ip":152,"op":165,"st":152,"bm":0,"sr":1},{"ddd":0,"ind":19,"ty":4,"nm":"I-B","parent":20,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[303.802,282.182,0]},"a":{"k":[303.802,282.182,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.859,-21.143],[-4.359,70.392]],"c":false}},"nm":"Path 1"},{"ty":"tm","s":{"k":[{"i":{"x":[0.12],"y":[0.12]},"o":{"x":[0.167],"y":[0.167]},"n":["0p12_0p12_0p167_0p167"],"t":150,"s":[0],"e":[0]},{"t":160}],"ix":1},"e":{"k":[{"i":{"x":[0.12],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p12_1_0p167_0p167"],"t":150,"s":[0],"e":[45.7]},{"t":160}],"ix":2},"o":{"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1"},{"ty":"st","fillEnabled":true,"c":{"k":[0,0.82,0.76,1]},"o":{"k":100},"w":{"k":9.194},"lc":3,"lj":1,"ml":10,"nm":"Stroke 1"},{"ty":"tr","p":{"k":[304.135,282.409],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6"}],"ip":150,"op":181,"st":87,"bm":0,"sr":1},{"ddd":0,"ind":20,"ty":4,"nm":"I-Y","parent":0,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[{"i":{"x":0.12,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p12_1_0p167_0p167","t":147,"s":[93.594,62.861,0],"e":[92.626,82.829,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.12,"y":1},"o":{"x":0.167,"y":0},"n":"0p12_1_0p167_0","t":157,"s":[92.626,82.829,0],"e":[92.844,77.861,0],"to":[0,0,0],"ti":[0,0,0]},{"t":161}]},"a":{"k":[303.802,282.182,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.859,-21.143],[-4.359,70.392]],"c":false}},"nm":"Path 1"},{"ty":"tm","s":{"k":[{"i":{"x":[0.12],"y":[0.12]},"o":{"x":[0.167],"y":[0.167]},"n":["0p12_0p12_0p167_0p167"],"t":147,"s":[0],"e":[0]},{"t":157}],"ix":1},"e":{"k":[{"i":{"x":[0.12],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p12_1_0p167_0p167"],"t":147,"s":[0],"e":[45.7]},{"t":157}],"ix":2},"o":{"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1"},{"ty":"st","fillEnabled":true,"c":{"k":[0,0.48,0.53,1]},"o":{"k":100},"w":{"k":8.4},"lc":3,"lj":1,"ml":10,"nm":"Stroke 1"},{"ty":"tr","p":{"k":[304.135,282.409],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6"}],"ip":147,"op":162,"st":84,"bm":0,"sr":1},{"ddd":0,"ind":21,"ty":4,"nm":"E3-B","parent":22,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[345.189,261.801,0]},"a":{"k":[345.124,261.801,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-13.664,-0.145],[75.663,0.29]],"c":false}},"nm":"Path 1"},{"ty":"tm","s":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":161,"s":[0],"e":[0]},{"t":166}],"ix":1},"e":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":161,"s":[0],"e":[31.6]},{"t":166}],"ix":2},"o":{"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 2"},{"ty":"st","fillEnabled":true,"c":{"k":[0,0.82,0.76,1]},"o":{"k":100},"w":{"k":9.562},"lc":2,"lj":1,"ml":10,"nm":"Stroke 1"},{"ty":"tr","p":{"k":[344.674,261.877],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1"}],"ip":161,"op":181,"st":98,"bm":0,"sr":1},{"ddd":0,"ind":22,"ty":4,"nm":"E3-Y","parent":0,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p667_1_0p167_0p167","t":153,"s":[119.167,57.479,0],"e":[137.167,57.479,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"n":"0p667_1_0p167_0","t":161,"s":[137.167,57.479,0],"e":[134.167,57.479,0],"to":[0,0,0],"ti":[0,0,0]},{"t":165}]},"a":{"k":[345.124,261.801,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-13.664,-0.145],[75.663,0.29]],"c":false}},"nm":"Path 1"},{"ty":"tm","s":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":153,"s":[0],"e":[0]},{"t":161}],"ix":1},"e":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":153,"s":[0],"e":[31.6]},{"t":161}],"ix":2},"o":{"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 2"},{"ty":"st","fillEnabled":true,"c":{"k":[0,0.48,0.53,1]},"o":{"k":100},"w":{"k":9.562},"lc":2,"lj":1,"ml":10,"nm":"Stroke 1"},{"ty":"tr","p":{"k":[344.674,261.877],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1"}],"ip":153,"op":171,"st":90,"bm":0,"sr":1},{"ddd":0,"ind":23,"ty":4,"nm":"Dot-Y","parent":24,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[{"i":{"x":0,"y":0.812},"o":{"x":0,"y":0},"n":"0_0p812_0_0","t":165,"s":[43.263,59.75,0],"e":[62.513,59.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.708,"y":1},"o":{"x":0.39,"y":0.303},"n":"0p708_1_0p39_0p303","t":177,"s":[62.513,59.75,0],"e":[63.763,59.75,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180}]},"a":{"k":[196.791,266.504,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"k":[9.2,9.2]},"p":{"k":[0.8,-0.5]},"nm":"Ellipse Path 1"},{"ty":"fl","fillEnabled":true,"c":{"k":[0,0.82,0.76,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[196,267],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1"}],"ip":165,"op":181,"st":134,"bm":0,"sr":1},{"ddd":0,"ind":24,"ty":1,"nm":"Bncr","parent":0,"ks":{"o":{"k":0},"r":{"k":0},"p":{"k":[{"i":{"x":0.18,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p18_1_0p167_0p167","t":165,"s":[164.782,57.473,0],"e":[164.782,55.473,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.82,"y":0},"n":"0p833_0p833_0p82_0","t":168,"s":[164.782,55.473,0],"e":[164.782,57.473,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.18,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p18_1_0p167_0p167","t":171,"s":[164.782,57.473,0],"e":[164.782,56.909,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.82,"y":0},"n":"0p833_0p833_0p82_0","t":174,"s":[164.782,56.909,0],"e":[164.782,57.473,0],"to":[0,0,0],"ti":[0,0,0]},{"t":177}]},"a":{"k":[60,60,0]},"s":{"k":[100,100,100]}},"ao":0,"sw":120,"sh":120,"sc":"#ffffff","ip":165,"op":181,"st":84,"bm":0,"sr":1},{"ddd":0,"ind":26,"ty":4,"nm":"Shape Layer 1","ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":0,"s":[1665.5,60,0],"e":[25.5,260,0],"to":[-273.333343505859,33.3333320617676,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":60,"s":[25.5,260,0],"e":[-3724.5,160,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"n":"0p833_0p833_0p333_0","t":120,"s":[-3724.5,160,0],"e":[-5224.5,160,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180}]},"a":{"k":[0,0,0]},"s":{"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":0,"s":[348,348,100],"e":[140,140,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":60,"s":[140,140,100],"e":[350,350,100]},{"t":120}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":{"i":[[0,0],[163.464,0],[160.759,0],[127.058,0],[141.102,0],[23.956,14.264],[1.023,0.84]],"o":[[0,0],[-163.464,0],[-160.76,0],[-117.888,0],[-105.065,0],[-16.798,-10.002],[0,0]],"v":[[798.186,62.256],[571.65,-57.36],[112.186,71.36],[-282.756,-71.36],[-619.479,67.362],[-823.238,21.419],[-899.622,-35.891]],"c":false}},"nm":"Path 1"},{"ty":"st","fillEnabled":true,"c":{"k":[0,0.82,0.76,1]},"o":{"k":100},"w":{"k":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1"},{"ty":"tr","p":{"k":[399.375,42.75],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1"},{"ty":"tm","s":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"n":["0p833_0p833_0p333_0"],"t":0,"s":[100],"e":[85.1]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p667_1_0p167_0p167"],"t":20,"s":[85.1],"e":[0]},{"t":60}],"ix":1},"e":{"k":[{"i":{"x":[0.667],"y":[0.667]},"o":{"x":[0.333],"y":[0.333]},"n":["0p667_0p667_0p333_0p333"],"t":0,"s":[100],"e":[100]},{"t":60}],"ix":2},"o":{"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1"}],"ip":0,"op":181,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":27,"ty":4,"nm":"Shape Layer 2","parent":26,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[{"i":{"x":0.833,"y":0.748},"o":{"x":0.64,"y":0},"n":"0p833_0p748_0p64_0","t":0,"s":[-424.344,63.484,0],"e":[-220.585,109.428,0],"to":[23.9563598632812,14.2642517089844,0],"ti":[-105.06494140625,0,0]},{"i":{"x":0.699,"y":1},"o":{"x":0.167,"y":0.118},"n":"0p699_1_0p167_0p118","t":32.938,"s":[-220.585,109.428,0],"e":[116.138,-29.294,0],"to":[141.102416992188,0,0],"ti":[-117.888366699219,0,0]},{"i":{"x":0.417,"y":0.338},"o":{"x":0.333,"y":0},"n":"0p417_0p338_0p333_0","t":60,"s":[116.138,-29.294,0],"e":[511.08,113.426,0],"to":[127.05810546875,0,0],"ti":[-160.759521484375,0,0]},{"i":{"x":0.608,"y":0.8},"o":{"x":0.211,"y":0.203},"n":"0p608_0p8_0p211_0p203","t":79.626,"s":[511.08,113.426,0],"e":[970.544,-15.294,0],"to":[160.759155273438,0,0],"ti":[-163.464111328125,0,0]},{"i":{"x":0.318,"y":1},"o":{"x":0.347,"y":0.767},"n":"0p318_1_0p347_0p767","t":98.478,"s":[970.544,-15.294,0],"e":[1093.398,11.416,0],"to":[49.9817848205566,0,0],"ti":[-32.2767219543457,-15.5276231765747,0]},{"i":{"x":0.833,"y":0.815},"o":{"x":0.333,"y":0},"n":"0p833_0p815_0p333_0","t":120,"s":[1093.398,11.416,0],"e":[1197.08,104.322,0],"to":[73.283447265625,35.2550582885742,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":135.517,"s":[1197.08,104.322,0],"e":[1231.937,166.608,0],"to":[0,0,0],"ti":[0,0,0]},{"t":141}]},"a":{"k":[-135.5,18.8,0]},"s":{"k":[28.736,28.736,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"k":[51.156,51.156]},"p":{"k":[0,0]},"nm":"Ellipse Path 1"},{"ty":"fl","fillEnabled":true,"c":{"k":[0,0.82,0.76,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[-135.5,-11.5],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1"}],"ip":0,"op":181,"st":0,"bm":0,"sr":1}],"v":"4.4.26","ddd":0,"ip":0,"op":181,"fr":30,"w":375,"h":500}
\ No newline at end of file
diff --git a/sample-compose/src/main/res/values/strings.xml b/sample-compose/src/main/res/values/strings.xml
index 4f9ee0d..7c07a76 100644
--- a/sample-compose/src/main/res/values/strings.xml
+++ b/sample-compose/src/main/res/values/strings.xml
@@ -1,10 +1,11 @@
 <resources>
     <string name="app_name">LottieCompose</string>
+    <string name="back">Back</string>
 
     <string name="bottom_tab_showcase">Showcase</string>
     <string name="bottom_tab_preview">Preview</string>
     <string name="bottom_tab_lottie_files">LottieFiles</string>
-    <string name="bottom_tab_docs">Docs</string>
+    <string name="bottom_tab_examples">Examples</string>
 
     <string name="tab_recent">Recent</string>
     <string name="tab_popular">Popular</string>
@@ -24,6 +25,8 @@
     <string name="toolbar_item_opacity_layers">Apply Opacity To Layers</string>
     <string name="toolbar_item_merge_paths">Enable Merge Paths</string>
 
+    <string name="examples_title">Examples</string>
+
     <string name="failed_to_load">Failed to load composition</string>
     <string name="ok">OK</string>
 </resources>
\ No newline at end of file