[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M71C3X2PQP07K8J7RGGS3MJQ":3,"skill-same:01M71C3X2PQP07K8J7RGGS3MJQ":56},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":29,"linkOnly":30,"markdownContent":31,"markdownBody":32,"frontMatter":33,"refName":34,"commitSha":35,"skillDirectory":36,"forksCount":37,"repositoryHtmlUrl":38,"repositoryHomepageUrl":6,"ownerHtmlUrl":39,"ownerBlogUrl":6,"ownerDescription":40,"updatedAt":41,"install":42,"resources":55},"01M71C3X2PQP07K8J7RGGS3MJQ","genjutsu-compose-motion",null,"Jetpack Compose animation foundations - animate*AsState, AnimatedVisibility, Crossfade, updateTransition, SharedTransitionLayout, gestures.","nexuslinkproductions\u002Fyuri-os","nexuslinkproductions","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F273246263?v=4","01M718YX9NZRCW50B59B8ZVDKN","Marcel Spatz","01M718YXAQ7530F4B1VKN6SFQV",2,"MIT","https:\u002F\u002Fgithub.com\u002Fnexuslinkproductions\u002Fyuri-os\u002Fblob\u002Fmain\u002Fskills\u002Fgenjutsu-compose-motion\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28],"compose","motion","genjutsu compose motion","gestures","Animation","animate","jetpack compose","jetpack","foundations","crossfade","2026-11-10 10:12:30",false,"---\nname: genjutsu-compose-motion\ndescription: \"Jetpack Compose animation foundations - animate*AsState, AnimatedVisibility, Crossfade, updateTransition, SharedTransitionLayout, gestures.\"\n---\n\n# Compose Motion - Sub-skill\n\n> Jetpack Compose animation core. Loaded for Android Compose and Compose Multiplatform projects.\n> Concise rules here. Deep-dive in `references\u002F`.\n> Baseline: Compose 1.7+ (April 2026 stable). Modern stable APIs only - no `swipeable`, no `animateContentSize` hacks where `AnimatedContent` is correct.\n\n---\n\n## API Decision Tree\n\n| Need | API |\n|---|---|\n| Single value over time | `animateFloatAsState`, `animateDpAsState`, `animateColorAsState`, etc. |\n| Visibility \u002F mount-unmount | `AnimatedVisibility(visible) { ... }` |\n| Crossfade between states | `Crossfade(target) { state -> ... }` |\n| Multi-state coordinated | `updateTransition(target).animateFloat { ... }` |\n| Manual control \u002F interruption | `Animatable(initialValue)` + `animateTo(...)` |\n| Looping \u002F infinite | `rememberInfiniteTransition().animateFloat(...)` |\n| Shared elements | `SharedTransitionLayout` + `Modifier.sharedElement(...)` (Compose 1.7+) |\n| Layout swap with anim | `AnimatedContent(target) { ... }` |\n| Drag \u002F swipe | `Modifier.draggable` + `Animatable.snapTo`\u002F`animateTo`, or `Modifier.anchoredDraggable` for snap-points |\n\n**Rule:** climb the ladder only when needed. `animate*AsState` covers 70% of cases. Reach for `Animatable` only when you need to interrupt, chain, or read velocity.\n\n---\n\n## Spring API (opinionated defaults)\n\n| Use | Spec |\n|---|---|\n| UI snap (modal, drawer, tab) | `spring(stiffness = Spring.StiffnessMediumLow, dampingRatio = Spring.DampingRatioNoBouncy)` |\n| Tactile (button press release, toggle) | `spring(stiffness = Spring.StiffnessMedium, dampingRatio = 0.85f)` |\n| Bouncy reveal (toast, FAB, success) | `spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy)` |\n| Drag follow (1:1 finger tracking) | `spring(stiffness = Spring.StiffnessHigh, dampingRatio = 1f)` |\n\nStiffness constants: `VeryLow` 200, `Low` 400, `MediumLow` 700, `Medium` 1500, `High` 10000. Higher = faster settle. Damping constants: `HighBouncy` 0.2, `MediumBouncy` 0.5, `LowBouncy` 0.75, `NoBouncy` 1.0. Below 1.0 overshoots. Springs ignore `durationMillis`; if you need a deterministic duration, use `tween(...)` instead.\n\n---\n\n## `animate*AsState` - The Bread and Butter\n\n```kotlin\nval targetAlpha = if (visible) 1f else 0f\nval alpha by animateFloatAsState(\n    targetValue = targetAlpha,\n    animationSpec = spring(stiffness = Spring.StiffnessMedium),\n    label = \"alpha\",\n)\nBox(modifier = Modifier.alpha(alpha))\n```\n\nThe `label` shows up in Layout Inspector \u002F Animation Preview - always set it, future-you will thank present-you. Variants ship for `Dp`, `Color`, `Offset`, `IntOffset`, `Size`, `Rect`, `Float`, `Int`, and a generic `animateValueAsState` for custom types via `TwoWayConverter`.\n\n---\n\n## `AnimatedVisibility` - Mount \u002F Unmount with Anim\n\n```kotlin\nAnimatedVisibility(\n    visible = expanded,\n    enter = slideInVertically(initialOffsetY = { -it }) + fadeIn(),\n    exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut(animationSpec = tween(150)),\n) {\n    Panel()\n}\n```\n\nCombine multiple enter\u002Fexit transitions with `+`. Respect motion-principles: exit shorter and simpler than enter (here 150ms tween fade vs full slide+fade enter). The content composable only runs while visible OR animating - safe to mount expensive children inside.\n\n---\n\n## `AnimatedContent` - State-Driven Layout Swap\n\n```kotlin\nAnimatedContent(\n    targetState = currentTab,\n    transitionSpec = {\n        (slideInHorizontally { it } + fadeIn()) togetherWith\n            (slideOutHorizontally { -it } + fadeOut())\n    },\n    label = \"tabs\",\n) { tab ->\n    TabContent(tab)\n}\n```\n\n`togetherWith` runs enter and exit in parallel; `using SizeTransform(clip = false)` controls how the container resizes between contents. Keys matter: if `targetState` doesn't change identity, no transition fires.\n\n---\n\n## `Crossfade` - Simple Fade Between States\n\n```kotlin\nCrossfade(targetState = isLoading, label = \"loadState\") { loading ->\n    if (loading) Spinner() else Content()\n}\n```\n\nUse when you only need a fade. For anything richer (slide, scale, layout-aware), reach for `AnimatedContent`. `Crossfade` does NOT animate size - the container takes the size of the new content immediately.\n\n---\n\n## `updateTransition` - Multi-Property Coordinated\n\n```kotlin\nval transition = updateTransition(targetState = expanded, label = \"expand\")\nval width by transition.animateDp(label = \"width\") { if (it) 300.dp else 100.dp }\nval color by transition.animateColor(label = \"color\") { if (it) Color.Blue else Color.Gray }\nval corner by transition.animateDp(label = \"corner\") { if (it) 24.dp else 8.dp }\n\nBox(\n    Modifier\n        .width(width)\n        .background(color, RoundedCornerShape(corner)),\n)\n```\n\nUse when several properties animate together based on the same state. All children share the same transition timeline, so they finish in sync. Each `animate*` call accepts its own `transitionSpec` lambda for per-property tuning.\n\n---\n\n## `Animatable` - Manual Control\n\n```kotlin\nval offsetX = remember { Animatable(0f) }\nLaunchedEffect(triggerEvent) {\n    offsetX.animateTo(100f, spring())\n    offsetX.animateTo(0f, spring(dampingRatio = Spring.DampingRatioMediumBouncy))\n}\nBox(Modifier.offset { IntOffset(offsetX.value.roundToInt(), 0) })\n```\n\nReach for `Animatable` when you need to interrupt (`stop()`), chain (`animateTo` returns when finished), read live velocity, or kick off decay (`animateDecay`). It is the imperative escape hatch for drag-then-fling, snap-back, and any flow `animate*AsState` cannot express.\n\n---\n\n## `SharedTransitionLayout` (Compose 1.7+) - Hero Animations\n\n```kotlin\nSharedTransitionLayout {\n    AnimatedContent(targetState = currentScreen, label = \"nav\") { screen ->\n        when (screen) {\n            Screen.List -> ListScreen(\n                sharedTransitionScope = this@SharedTransitionLayout,\n                animatedVisibilityScope = this@AnimatedContent,\n            )\n            is Screen.Detail -> DetailScreen(\n                item = screen.item,\n                sharedTransitionScope = this@SharedTransitionLayout,\n                animatedVisibilityScope = this@AnimatedContent,\n            )\n        }\n    }\n}\n\n\u002F\u002F Inside ListScreen, on the card image:\nwith(sharedTransitionScope) {\n    Image(\n        painter = painter,\n        contentDescription = null,\n        modifier = Modifier.sharedElement(\n            state = rememberSharedContentState(key = \"hero-${item.id}\"),\n            animatedVisibilityScope = animatedVisibilityScope,\n        ),\n    )\n}\n```\n\nTwo scopes plumbed down: `SharedTransitionScope` (where `Modifier.sharedElement` extension lives) and `AnimatedVisibilityScope` (the visibility context that drives the transition). `rememberSharedContentState(key)` keys MUST match across screens or no animation fires. Deep-dive in `references\u002Fshared-transitions.md`.\n\n---\n\n## `InfiniteTransition` - Looping\n\n```kotlin\nval infinite = rememberInfiniteTransition(label = \"loader\")\nval rotation by infinite.animateFloat(\n    initialValue = 0f,\n    targetValue = 360f,\n    animationSpec = infiniteRepeatable(\n        animation = tween(1000, easing = LinearEasing),\n        repeatMode = RepeatMode.Restart,\n    ),\n    label = \"rotation\",\n)\nIcon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.rotate(rotation))\n```\n\n`RepeatMode.Restart` jumps back to start each cycle; `RepeatMode.Reverse` ping-pongs. Always provide a pause \u002F off path for accessibility (see Reduced Motion section below). For one-shot decay (fling), use `Animatable.animateDecay` instead.\n\n---\n\n## Gestures (quick map)\n\n| Need | Modifier |\n|---|---|\n| Tap | `Modifier.clickable(onClick = ...)` |\n| Tap + long-press | `Modifier.combinedClickable(onClick = ..., onLongClick = ...)` |\n| Drag (single axis) | `Modifier.draggable(state, orientation)` |\n| Multi-touch \u002F custom | `Modifier.pointerInput { detectDragGestures { ... } }` |\n| Scroll offset reading | `LazyListState` or `ScrollState.value` |\n| Swipe-to-dismiss \u002F snap points | `Modifier.anchoredDraggable(state)` (Compose 1.6+, replaces `swipeable`) |\n| Pinch + pan + rotate | `Modifier.transformable(state)` |\n\nDeep-dive in `references\u002Fgestures-compose.md` (NestedScrollConnection, conflict resolution, fling decay).\n\n---\n\n## Anti-Patterns (BAD \u002F GOOD)\n\n### 1. Animating layout size instead of transform\n\n```kotlin\n\u002F\u002F BAD - animateDpAsState on width forces a layout pass every frame\nval w by animateDpAsState(if (expanded) 300.dp else 100.dp)\nBox(Modifier.width(w).height(60.dp))\n```\n\n```kotlin\n\u002F\u002F GOOD - animate scale via graphicsLayer (composite-only, no layout)\nval scale by animateFloatAsState(if (expanded) 3f else 1f, label = \"scale\")\nBox(\n    Modifier\n        .width(100.dp)\n        .height(60.dp)\n        .graphicsLayer {\n            scaleX = scale\n            transformOrigin = TransformOrigin(0f, 0.5f)\n        },\n)\n```\n\nIf the actual layout size MUST change (parent depends on it), use `Modifier.animateContentSize()` so neighbors animate too, or wrap the swap in `AnimatedContent`.\n\n### 2. `LaunchedEffect(true)` \u002F `LaunchedEffect(Unit)` with hidden inputs\n\n```kotlin\n\u002F\u002F BAD - re-runs only once, BUT also fires on every recomposition surprise\n\u002F\u002F        when callers swap the composable instance. Worse: anything captured\n\u002F\u002F        in the lambda is stale.\nLaunchedEffect(true) {\n    offsetX.animateTo(target)\n}\n```\n\n```kotlin\n\u002F\u002F GOOD - explicit key tied to the trigger\nLaunchedEffect(triggerKey) {\n    offsetX.animateTo(target)\n}\n```\n\nIf you genuinely want \"run once\", use `LaunchedEffect(Unit)` on purpose AND ensure you don't capture varying state - or hoist the captured values out. When in doubt, key on the values you read.\n\n### 3. Forgetting `key` in a `LazyColumn` with item animations\n\n```kotlin\n\u002F\u002F BAD - on insert\u002Fdelete\u002Freorder, items animate to wrong slots\nLazyColumn {\n    items(list) { item ->\n        AnimatedVisibility(visible = item.expanded) { ItemRow(item) }\n    }\n}\n```\n\n```kotlin\n\u002F\u002F GOOD - stable key by id, plus Modifier.animateItem for reorder anim\nLazyColumn {\n    items(list, key = { it.id }) { item ->\n        Row(modifier = Modifier.animateItem()) {\n            AnimatedVisibility(visible = item.expanded) { ItemRow(item) }\n        }\n    }\n}\n```\n\n`Modifier.animateItem()` (Compose 1.7+, replaces `animateItemPlacement`) handles insert\u002Fremove\u002Fmove automatically when `key` is stable.\n\n### 4. Nesting `AnimatedContent` inside scrolling list items\n\n```kotlin\n\u002F\u002F BAD - every item runs its own transition graph; scroll = jank\nLazyColumn {\n    items(list, key = { it.id }) { item ->\n        AnimatedContent(targetState = item.state) { state -> Row(state) }\n    }\n}\n```\n\n```kotlin\n\u002F\u002F GOOD - lift state, animate only the changing prop on the row\nLazyColumn {\n    items(list, key = { it.id }) { item ->\n        val color by animateColorAsState(if (item.selected) selBg else bg, label = \"rowBg\")\n        Row(Modifier.background(color)) { Content(item) }\n    }\n}\n```\n\nRule: heavy animation containers (`AnimatedContent`, `SharedTransitionLayout`) belong at screen scope, not per row.\n\n---\n\n## Reduced Motion - Respect It\n\nSee `..\u002Fmotion-principles\u002FSKILL.md` for the cross-platform doctrine.\n\n```kotlin\n@Composable\nfun rememberReduceMotion(): Boolean {\n    val context = LocalContext.current\n    return remember {\n        Settings.Global.getFloat(\n            context.contentResolver,\n            Settings.Global.ANIMATOR_DURATION_SCALE,\n            1f,\n        ) == 0f\n    }\n}\n\nval reduce = rememberReduceMotion()\nval alpha by animateFloatAsState(\n    targetValue = if (visible) 1f else 0f,\n    animationSpec = if (reduce) snap() else spring(),\n    label = \"alpha\",\n)\n```\n\n`ANIMATOR_DURATION_SCALE` covers the developer-options \"Animations off\" toggle. For the user-facing accessibility setting on Android 14+ (\"Remove animations\" in Settings -> Accessibility), use `AccessibilityManager.areTransitionsEnabled()` (API 34+). Deep-dive in `..\u002Fmobile-principles\u002Freferences\u002Faccessibility-mobile.md`.\n\n---\n\n## Quick Reference\n\n| Need | Load |\n|---|---|\n| Shared transitions deep-dive | `references\u002Fshared-transitions.md` |\n| Gestures + nestedScroll patterns | `references\u002Fgestures-compose.md` |\n| Recomposition \u002F jank \u002F Layout Inspector | `references\u002Frecomposition-and-anim.md` |\n| Advanced (M3 Expressive, AGSL, Canvas) | `..\u002Fcompose-graphics\u002FSKILL.md` |\n| CMP \u002F KMP patterns | `..\u002Fcompose-multiplatform\u002FSKILL.md` |\n| Mobile UX context | `..\u002Fmobile-principles\u002FSKILL.md` |\n| Foundation (timing, easing, a11y) | `..\u002Fmotion-principles\u002FSKILL.md` |\n\n---\n\n## Sources\n\n- [aldefy\u002Fcompose-skill](https:\u002F\u002Fgithub.com\u002Faldefy\u002Fcompose-skill) - animation reference\n- [skydoves\u002FOrbital](https:\u002F\u002Fgithub.com\u002Fskydoves\u002FOrbital) - shared element transitions\n- [mutualmobile\u002Fcompose-animation-examples](https:\u002F\u002Fgithub.com\u002Fmutualmobile\u002Fcompose-animation-examples)\n- [fornewid\u002Fmaterial-motion-compose](https:\u002F\u002Fgithub.com\u002Ffornewid\u002Fmaterial-motion-compose)\n- [Android Compose animations docs](https:\u002F\u002Fdeveloper.android.com\u002Fdevelop\u002Fui\u002Fcompose\u002Fanimation\u002Fintroduction)\n- [Android SharedTransitionLayout](https:\u002F\u002Fdeveloper.android.com\u002Fdevelop\u002Fui\u002Fcompose\u002Fanimation\u002Fshared-elements)\n","# Compose Motion - Sub-skill\n\n> Jetpack Compose animation core. Loaded for Android Compose and Compose Multiplatform projects.\n> Concise rules here. Deep-dive in `references\u002F`.\n> Baseline: Compose 1.7+ (April 2026 stable). Modern stable APIs only - no `swipeable`, no `animateContentSize` hacks where `AnimatedContent` is correct.\n\n---\n\n## API Decision Tree\n\n| Need | API |\n|---|---|\n| Single value over time | `animateFloatAsState`, `animateDpAsState`, `animateColorAsState`, etc. |\n| Visibility \u002F mount-unmount | `AnimatedVisibility(visible) { ... }` |\n| Crossfade between states | `Crossfade(target) { state -> ... }` |\n| Multi-state coordinated | `updateTransition(target).animateFloat { ... }` |\n| Manual control \u002F interruption | `Animatable(initialValue)` + `animateTo(...)` |\n| Looping \u002F infinite | `rememberInfiniteTransition().animateFloat(...)` |\n| Shared elements | `SharedTransitionLayout` + `Modifier.sharedElement(...)` (Compose 1.7+) |\n| Layout swap with anim | `AnimatedContent(target) { ... }` |\n| Drag \u002F swipe | `Modifier.draggable` + `Animatable.snapTo`\u002F`animateTo`, or `Modifier.anchoredDraggable` for snap-points |\n\n**Rule:** climb the ladder only when needed. `animate*AsState` covers 70% of cases. Reach for `Animatable` only when you need to interrupt, chain, or read velocity.\n\n---\n\n## Spring API (opinionated defaults)\n\n| Use | Spec |\n|---|---|\n| UI snap (modal, drawer, tab) | `spring(stiffness = Spring.StiffnessMediumLow, dampingRatio = Spring.DampingRatioNoBouncy)` |\n| Tactile (button press release, toggle) | `spring(stiffness = Spring.StiffnessMedium, dampingRatio = 0.85f)` |\n| Bouncy reveal (toast, FAB, success) | `spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy)` |\n| Drag follow (1:1 finger tracking) | `spring(stiffness = Spring.StiffnessHigh, dampingRatio = 1f)` |\n\nStiffness constants: `VeryLow` 200, `Low` 400, `MediumLow` 700, `Medium` 1500, `High` 10000. Higher = faster settle. Damping constants: `HighBouncy` 0.2, `MediumBouncy` 0.5, `LowBouncy` 0.75, `NoBouncy` 1.0. Below 1.0 overshoots. Springs ignore `durationMillis`; if you need a deterministic duration, use `tween(...)` instead.\n\n---\n\n## `animate*AsState` - The Bread and Butter\n\n```kotlin\nval targetAlpha = if (visible) 1f else 0f\nval alpha by animateFloatAsState(\n    targetValue = targetAlpha,\n    animationSpec = spring(stiffness = Spring.StiffnessMedium),\n    label = \"alpha\",\n)\nBox(modifier = Modifier.alpha(alpha))\n```\n\nThe `label` shows up in Layout Inspector \u002F Animation Preview - always set it, future-you will thank present-you. Variants ship for `Dp`, `Color`, `Offset`, `IntOffset`, `Size`, `Rect`, `Float`, `Int`, and a generic `animateValueAsState` for custom types via `TwoWayConverter`.\n\n---\n\n## `AnimatedVisibility` - Mount \u002F Unmount with Anim\n\n```kotlin\nAnimatedVisibility(\n    visible = expanded,\n    enter = slideInVertically(initialOffsetY = { -it }) + fadeIn(),\n    exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut(animationSpec = tween(150)),\n) {\n    Panel()\n}\n```\n\nCombine multiple enter\u002Fexit transitions with `+`. Respect motion-principles: exit shorter and simpler than enter (here 150ms tween fade vs full slide+fade enter). The content composable only runs while visible OR animating - safe to mount expensive children inside.\n\n---\n\n## `AnimatedContent` - State-Driven Layout Swap\n\n```kotlin\nAnimatedContent(\n    targetState = currentTab,\n    transitionSpec = {\n        (slideInHorizontally { it } + fadeIn()) togetherWith\n            (slideOutHorizontally { -it } + fadeOut())\n    },\n    label = \"tabs\",\n) { tab ->\n    TabContent(tab)\n}\n```\n\n`togetherWith` runs enter and exit in parallel; `using SizeTransform(clip = false)` controls how the container resizes between contents. Keys matter: if `targetState` doesn't change identity, no transition fires.\n\n---\n\n## `Crossfade` - Simple Fade Between States\n\n```kotlin\nCrossfade(targetState = isLoading, label = \"loadState\") { loading ->\n    if (loading) Spinner() else Content()\n}\n```\n\nUse when you only need a fade. For anything richer (slide, scale, layout-aware), reach for `AnimatedContent`. `Crossfade` does NOT animate size - the container takes the size of the new content immediately.\n\n---\n\n## `updateTransition` - Multi-Property Coordinated\n\n```kotlin\nval transition = updateTransition(targetState = expanded, label = \"expand\")\nval width by transition.animateDp(label = \"width\") { if (it) 300.dp else 100.dp }\nval color by transition.animateColor(label = \"color\") { if (it) Color.Blue else Color.Gray }\nval corner by transition.animateDp(label = \"corner\") { if (it) 24.dp else 8.dp }\n\nBox(\n    Modifier\n        .width(width)\n        .background(color, RoundedCornerShape(corner)),\n)\n```\n\nUse when several properties animate together based on the same state. All children share the same transition timeline, so they finish in sync. Each `animate*` call accepts its own `transitionSpec` lambda for per-property tuning.\n\n---\n\n## `Animatable` - Manual Control\n\n```kotlin\nval offsetX = remember { Animatable(0f) }\nLaunchedEffect(triggerEvent) {\n    offsetX.animateTo(100f, spring())\n    offsetX.animateTo(0f, spring(dampingRatio = Spring.DampingRatioMediumBouncy))\n}\nBox(Modifier.offset { IntOffset(offsetX.value.roundToInt(), 0) })\n```\n\nReach for `Animatable` when you need to interrupt (`stop()`), chain (`animateTo` returns when finished), read live velocity, or kick off decay (`animateDecay`). It is the imperative escape hatch for drag-then-fling, snap-back, and any flow `animate*AsState` cannot express.\n\n---\n\n## `SharedTransitionLayout` (Compose 1.7+) - Hero Animations\n\n```kotlin\nSharedTransitionLayout {\n    AnimatedContent(targetState = currentScreen, label = \"nav\") { screen ->\n        when (screen) {\n            Screen.List -> ListScreen(\n                sharedTransitionScope = this@SharedTransitionLayout,\n                animatedVisibilityScope = this@AnimatedContent,\n            )\n            is Screen.Detail -> DetailScreen(\n                item = screen.item,\n                sharedTransitionScope = this@SharedTransitionLayout,\n                animatedVisibilityScope = this@AnimatedContent,\n            )\n        }\n    }\n}\n\n\u002F\u002F Inside ListScreen, on the card image:\nwith(sharedTransitionScope) {\n    Image(\n        painter = painter,\n        contentDescription = null,\n        modifier = Modifier.sharedElement(\n            state = rememberSharedContentState(key = \"hero-${item.id}\"),\n            animatedVisibilityScope = animatedVisibilityScope,\n        ),\n    )\n}\n```\n\nTwo scopes plumbed down: `SharedTransitionScope` (where `Modifier.sharedElement` extension lives) and `AnimatedVisibilityScope` (the visibility context that drives the transition). `rememberSharedContentState(key)` keys MUST match across screens or no animation fires. Deep-dive in `references\u002Fshared-transitions.md`.\n\n---\n\n## `InfiniteTransition` - Looping\n\n```kotlin\nval infinite = rememberInfiniteTransition(label = \"loader\")\nval rotation by infinite.animateFloat(\n    initialValue = 0f,\n    targetValue = 360f,\n    animationSpec = infiniteRepeatable(\n        animation = tween(1000, easing = LinearEasing),\n        repeatMode = RepeatMode.Restart,\n    ),\n    label = \"rotation\",\n)\nIcon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.rotate(rotation))\n```\n\n`RepeatMode.Restart` jumps back to start each cycle; `RepeatMode.Reverse` ping-pongs. Always provide a pause \u002F off path for accessibility (see Reduced Motion section below). For one-shot decay (fling), use `Animatable.animateDecay` instead.\n\n---\n\n## Gestures (quick map)\n\n| Need | Modifier |\n|---|---|\n| Tap | `Modifier.clickable(onClick = ...)` |\n| Tap + long-press | `Modifier.combinedClickable(onClick = ..., onLongClick = ...)` |\n| Drag (single axis) | `Modifier.draggable(state, orientation)` |\n| Multi-touch \u002F custom | `Modifier.pointerInput { detectDragGestures { ... } }` |\n| Scroll offset reading | `LazyListState` or `ScrollState.value` |\n| Swipe-to-dismiss \u002F snap points | `Modifier.anchoredDraggable(state)` (Compose 1.6+, replaces `swipeable`) |\n| Pinch + pan + rotate | `Modifier.transformable(state)` |\n\nDeep-dive in `references\u002Fgestures-compose.md` (NestedScrollConnection, conflict resolution, fling decay).\n\n---\n\n## Anti-Patterns (BAD \u002F GOOD)\n\n### 1. Animating layout size instead of transform\n\n```kotlin\n\u002F\u002F BAD - animateDpAsState on width forces a layout pass every frame\nval w by animateDpAsState(if (expanded) 300.dp else 100.dp)\nBox(Modifier.width(w).height(60.dp))\n```\n\n```kotlin\n\u002F\u002F GOOD - animate scale via graphicsLayer (composite-only, no layout)\nval scale by animateFloatAsState(if (expanded) 3f else 1f, label = \"scale\")\nBox(\n    Modifier\n        .width(100.dp)\n        .height(60.dp)\n        .graphicsLayer {\n            scaleX = scale\n            transformOrigin = TransformOrigin(0f, 0.5f)\n        },\n)\n```\n\nIf the actual layout size MUST change (parent depends on it), use `Modifier.animateContentSize()` so neighbors animate too, or wrap the swap in `AnimatedContent`.\n\n### 2. `LaunchedEffect(true)` \u002F `LaunchedEffect(Unit)` with hidden inputs\n\n```kotlin\n\u002F\u002F BAD - re-runs only once, BUT also fires on every recomposition surprise\n\u002F\u002F        when callers swap the composable instance. Worse: anything captured\n\u002F\u002F        in the lambda is stale.\nLaunchedEffect(true) {\n    offsetX.animateTo(target)\n}\n```\n\n```kotlin\n\u002F\u002F GOOD - explicit key tied to the trigger\nLaunchedEffect(triggerKey) {\n    offsetX.animateTo(target)\n}\n```\n\nIf you genuinely want \"run once\", use `LaunchedEffect(Unit)` on purpose AND ensure you don't capture varying state - or hoist the captured values out. When in doubt, key on the values you read.\n\n### 3. Forgetting `key` in a `LazyColumn` with item animations\n\n```kotlin\n\u002F\u002F BAD - on insert\u002Fdelete\u002Freorder, items animate to wrong slots\nLazyColumn {\n    items(list) { item ->\n        AnimatedVisibility(visible = item.expanded) { ItemRow(item) }\n    }\n}\n```\n\n```kotlin\n\u002F\u002F GOOD - stable key by id, plus Modifier.animateItem for reorder anim\nLazyColumn {\n    items(list, key = { it.id }) { item ->\n        Row(modifier = Modifier.animateItem()) {\n            AnimatedVisibility(visible = item.expanded) { ItemRow(item) }\n        }\n    }\n}\n```\n\n`Modifier.animateItem()` (Compose 1.7+, replaces `animateItemPlacement`) handles insert\u002Fremove\u002Fmove automatically when `key` is stable.\n\n### 4. Nesting `AnimatedContent` inside scrolling list items\n\n```kotlin\n\u002F\u002F BAD - every item runs its own transition graph; scroll = jank\nLazyColumn {\n    items(list, key = { it.id }) { item ->\n        AnimatedContent(targetState = item.state) { state -> Row(state) }\n    }\n}\n```\n\n```kotlin\n\u002F\u002F GOOD - lift state, animate only the changing prop on the row\nLazyColumn {\n    items(list, key = { it.id }) { item ->\n        val color by animateColorAsState(if (item.selected) selBg else bg, label = \"rowBg\")\n        Row(Modifier.background(color)) { Content(item) }\n    }\n}\n```\n\nRule: heavy animation containers (`AnimatedContent`, `SharedTransitionLayout`) belong at screen scope, not per row.\n\n---\n\n## Reduced Motion - Respect It\n\nSee `..\u002Fmotion-principles\u002FSKILL.md` for the cross-platform doctrine.\n\n```kotlin\n@Composable\nfun rememberReduceMotion(): Boolean {\n    val context = LocalContext.current\n    return remember {\n        Settings.Global.getFloat(\n            context.contentResolver,\n            Settings.Global.ANIMATOR_DURATION_SCALE,\n            1f,\n        ) == 0f\n    }\n}\n\nval reduce = rememberReduceMotion()\nval alpha by animateFloatAsState(\n    targetValue = if (visible) 1f else 0f,\n    animationSpec = if (reduce) snap() else spring(),\n    label = \"alpha\",\n)\n```\n\n`ANIMATOR_DURATION_SCALE` covers the developer-options \"Animations off\" toggle. For the user-facing accessibility setting on Android 14+ (\"Remove animations\" in Settings -> Accessibility), use `AccessibilityManager.areTransitionsEnabled()` (API 34+). Deep-dive in `..\u002Fmobile-principles\u002Freferences\u002Faccessibility-mobile.md`.\n\n---\n\n## Quick Reference\n\n| Need | Load |\n|---|---|\n| Shared transitions deep-dive | `references\u002Fshared-transitions.md` |\n| Gestures + nestedScroll patterns | `references\u002Fgestures-compose.md` |\n| Recomposition \u002F jank \u002F Layout Inspector | `references\u002Frecomposition-and-anim.md` |\n| Advanced (M3 Expressive, AGSL, Canvas) | `..\u002Fcompose-graphics\u002FSKILL.md` |\n| CMP \u002F KMP patterns | `..\u002Fcompose-multiplatform\u002FSKILL.md` |\n| Mobile UX context | `..\u002Fmobile-principles\u002FSKILL.md` |\n| Foundation (timing, easing, a11y) | `..\u002Fmotion-principles\u002FSKILL.md` |\n\n---\n\n## Sources\n\n- [aldefy\u002Fcompose-skill](https:\u002F\u002Fgithub.com\u002Faldefy\u002Fcompose-skill) - animation reference\n- [skydoves\u002FOrbital](https:\u002F\u002Fgithub.com\u002Fskydoves\u002FOrbital) - shared element transitions\n- [mutualmobile\u002Fcompose-animation-examples](https:\u002F\u002Fgithub.com\u002Fmutualmobile\u002Fcompose-animation-examples)\n- [fornewid\u002Fmaterial-motion-compose](https:\u002F\u002Fgithub.com\u002Ffornewid\u002Fmaterial-motion-compose)\n- [Android Compose animations docs](https:\u002F\u002Fdeveloper.android.com\u002Fdevelop\u002Fui\u002Fcompose\u002Fanimation\u002Fintroduction)\n- [Android SharedTransitionLayout](https:\u002F\u002Fdeveloper.android.com\u002Fdevelop\u002Fui\u002Fcompose\u002Fanimation\u002Fshared-elements)\n",{"name":5,"description":7},"main","4c6d8d65f3d6096e2fc25b0845e481dda73c2794","skills\u002Fgenjutsu-compose-motion",0,"https:\u002F\u002Fgithub.com\u002Fnexuslinkproductions\u002Fyuri-os","https:\u002F\u002Fgithub.com\u002Fnexuslinkproductions","Building deterministic AI governance and continuity systems. Workflow architect + visual storyteller","2026-11-14 03:05:17",{"source":8,"skillName":5,"commands":43,"prompt":53,"zipAvailable":54},[44,47,50],{"tool":45,"command":46},"npx","npx skills add nexuslinkproductions\u002Fyuri-os --skill genjutsu-compose-motion",{"tool":48,"command":49},"bunx","bunx skills add nexuslinkproductions\u002Fyuri-os --skill genjutsu-compose-motion",{"tool":51,"command":52},"pnpm","pnpm dlx skills add nexuslinkproductions\u002Fyuri-os --skill genjutsu-compose-motion","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fnexuslinkproductions\u002Fyuri-os\u002Fblob\u002Fmain\u002Fskills\u002Fgenjutsu-compose-motion\u002FSKILL.md",true,[],[57,80,104,134,163,182],{"skillNo":58,"name":59,"nameZh":6,"description":60,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":61,"categories":62,"keywords":63,"publishedAt":29},"01M71CG1AME5WC0A79AE65WT0X","zoom-out","Tell the agent to zoom out and give broader context or a higher-level perspective. Use when you're unfamiliar with a section of code or need to understand how it fits into the bigger picture.","https:\u002F\u002Fgithub.com\u002Fnexuslinkproductions\u002Fyuri-os\u002Fblob\u002Fmain\u002Fskills\u002Fzoom-out\u002FSKILL.md",[],[64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79],"zoom out","zoom","with","harness","how","section","re","true","understand","use","perspective","when","you","code","it","context",{"skillNo":81,"name":82,"nameZh":6,"description":83,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":84,"categories":85,"keywords":86,"publishedAt":29},"01M71CFY9ATD0BAXV6B14HFG7C","yuri-shura","6-perspective adversarial review for high-stakes turns (architecture decisions, refactor planning, deployment review) — fans out architect, adversary (7-vector attack), maintainer, ops, product, and security perspectives in parallel, then consolidates. Use when user says 'review this decision', '\u002Fshura', 'adversarial review', 'what could go wrong', 'architecture review', or 'deployment review'.","https:\u002F\u002Fgithub.com\u002Fnexuslinkproductions\u002Fyuri-os\u002Fblob\u002Fmain\u002Fskills\u002Fyuri-shura\u002FSKILL.md",[],[87,88,67,89,90,91,92,93,94,95,96,73,97,75,98,99,100,101,102,103],"yuri shura","parallel","architecture review","planning","for","deployment","vector","strategic review","decision","strategic","ops","workflow","decisions","architecture","review","perspectives","adversarial",{"skillNo":105,"name":106,"nameZh":6,"description":107,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":108,"categories":109,"keywords":115,"publishedAt":29},"01M71CFTTYW1HA2MNX38EV1GJ5","yuri-sales-intelligence","Unified sales and outreach skill generating cold outreach openers, social selling sequences, objection rebuttals, and ad creative for any paid platform — multi-persona aware, adapting tone and offer frame to buyer type. Use when user says 'write a cold email', 'outreach sequence', 'objection handling', 'ad copy', or 'sales pitch'.","https:\u002F\u002Fgithub.com\u002Fnexuslinkproductions\u002Fyuri-os\u002Fblob\u002Fmain\u002Fskills\u002Fyuri-sales-intelligence\u002FSKILL.md",[110],{"categoryNo":111,"categoryName":112,"taxonomyType":113,"taxonomyTypeDesc":114},"01JBSEEDCAT000000000000005","邮件与沟通","SCENARIO","使用场景",[116,117,118,119,120,121,122,123,124,75,125,126,127,128,129,130,131,91,132,133],"intelligence","sales intelligence","yuri sales intelligence","sales","cold","paid","social","persona","pitch","cold email","ad creative","write","sequences","sequence","platform","email","cold outreach","frame",{"skillNo":135,"name":136,"nameZh":6,"description":137,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":138,"categories":139,"keywords":148,"publishedAt":29},"01M71CFQMBFTFADJNGP0Z2WCYK","yuri-code-intelligence","Unified code quality intelligence layer that diagnoses code smells (Long Method, Feature Envy, design pattern issues), prescribes targeted Fowler refactoring techniques, and reviews code for security, performance, and clarity. Use when user says 'review this code', 'refactor this', 'what's wrong with this method', 'code smells', or 'clean this up'.","https:\u002F\u002Fgithub.com\u002Fnexuslinkproductions\u002Fyuri-os\u002Fblob\u002Fmain\u002Fskills\u002Fyuri-code-intelligence\u002FSKILL.md",[140,143],{"categoryNo":141,"categoryName":142,"taxonomyType":113,"taxonomyTypeDesc":114},"01JBSEEDCAT000000000000007","代码审查",{"categoryNo":144,"categoryName":145,"taxonomyType":146,"taxonomyTypeDesc":147},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[149,77,150,116,67,151,152,153,91,154,155,156,157,158,159,160,161,162,73,75],"yuri code intelligence","code intelligence","reviews","code quality","feature","performance","code review","long","up","issues","clean","techniques","improve","design",{"skillNo":164,"name":165,"nameZh":6,"description":166,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":167,"categories":168,"keywords":169,"publishedAt":29},"01M71CFP32RT6GD3GKNHCJP8VP","yuri-adw","Invoke when user asks to run the ADW loop \u002F agentic engineering loop \u002F software factory workflow.","https:\u002F\u002Fgithub.com\u002Fnexuslinkproductions\u002Fyuri-os\u002Fblob\u002Fmain\u002Fskills\u002Fyuri-adw\u002FSKILL.md",[],[170,171,172,173,174,175,176,177,178,75,179,98,180,181],"yuri adw","run","engineering loop","software factory","software","agentic engineering","factory","loop","invoke","engineering","agentic","user",{"skillNo":183,"name":184,"nameZh":6,"description":185,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":186,"categories":187,"keywords":200,"publishedAt":29},"01M71CFK4VPEKY6A73237P7RSN","writing-skills","Use when creating new skills, editing existing skills, or verifying skills work before deployment","https:\u002F\u002Fgithub.com\u002Fnexuslinkproductions\u002Fyuri-os\u002Fblob\u002Fmain\u002Fskills\u002Fwriting-skills\u002FSKILL.md",[188,191,194,197],{"categoryNo":189,"categoryName":190,"taxonomyType":113,"taxonomyTypeDesc":114},"01JBSEEDCAT000000000000008","测试与质量",{"categoryNo":192,"categoryName":193,"taxonomyType":113,"taxonomyTypeDesc":114},"01JBSEEDCAT000000000000018","写作与文案",{"categoryNo":195,"categoryName":196,"taxonomyType":113,"taxonomyTypeDesc":114},"01JBSEEDCAT000000000000023","Skill与智能体开发",{"categoryNo":198,"categoryName":199,"taxonomyType":146,"taxonomyTypeDesc":147},"01JBSEEDCAT000000000000036","内容创作者",[201,202,203,75,98,204,67,205,92,73,206],"writing","writing skills","new","work","editing","Existing"]