# Micro-Animations Design — Swoosh (v2)

**Date:** 2026-03-19
**Status:** Approved
**Style:** Expressive & playful — bouncy springs, overshoot, lively feedback

## Overview

Add 32 micro-animations + 8 navigation transitions across all screens to make the app feel alive. Create a shared `SwooshMotion` object with predefined animation specs ensuring consistent character throughout the app.

The app now includes: multi-account support, search with filters, swipe-to-dismiss, multi-image galleries, writing statistics dashboard, HTML preview, and hashtag support.

## Shared Animation Specs — SwooshMotion

Central object in `ui/animation/SwooshMotion.kt`:

| Name | Type | Parameters | Use case |
|------|------|-----------|----------|
| `Bouncy` | Spring | dampingRatio=0.65, stiffness=400 | FAB entrance, chips, badges |
| `BouncyQuick` | Spring | dampingRatio=0.7, stiffness=1000 | Press feedback (~150ms settle) |
| `Snappy` | Spring | dampingRatio=0.7, stiffness=800 | Expand/collapse, dialogs |
| `Gentle` | Spring | dampingRatio=0.8, stiffness=300 | Cards, content reveal |
| `Quick` | Tween | 200ms, FastOutSlowInEasing | Fade, color transitions |
| `StaggerDelayMs` | Long | 50 | List item entrances |
| `RevealDelayMs` | Long | 80 | Content reveal sequences |

### Reduced Motion Support

`SwooshMotion` checks `Settings.Global.ANIMATOR_DURATION_SCALE`. When reduced motion is enabled, all springs fall back to `snap()` and all tweens use 0ms duration.

### Stagger Pattern

Each item uses a `MutableState<Boolean>` toggled via `LaunchedEffect(Unit) { delay(StaggerDelayMs * index); visible = true }`. `AnimatedVisibility` has no built-in delay.

### Already-Animated Tracking (LazyColumn)

Hoist a `mutableStateMapOf<String, Boolean>()` of already-animated item keys in `FeedScreen`. Check `key !in animatedKeys` before triggering entrance. Only first ~8 visible items on initial load animate. Items from infinite scroll appear instantly.

---

## Feed Screen — 12 animations

### F1. FAB entrance (SPRING/Bouncy)
Scale 0→1 with overshoot on screen open. Hidden during search mode.
- `animateFloatAsState` with `Bouncy` spring on scale modifier
- `LaunchedEffect(Unit)` sets target to 1f
- FAB already conditionally hidden when `isSearchActive` (line 249)

### F2. FAB press (SPRING/BouncyQuick)
Shrinks to 85%, snaps back. Settles before navigation fires.
- `Modifier.pointerInput` press → `animateFloatAsState` 0.85f→1f
- Navigation fires immediately; animation interrupted by transition (fine)

### F3. Post cards staggered entrance (SLIDE/Gentle)
Cascading slide-in from bottom, 50ms delay per item. "Waterfall" effect.
- `AnimatedVisibility` per item in LazyColumn with `slideInVertically + fadeIn`
- `mutableStateMapOf<String, Boolean>` tracking (see shared section)
- Applies to both `SwipeablePostCard` and search-mode `PostCard`
- Capped at first 8 items, infinite scroll items appear instantly

### F4. "Show more" expand (SPRING/Snappy)
`AnimatedContent(targetState = expanded)` with `fadeIn + expandVertically` / `fadeOut + shrinkVertically`. Replaces discrete text swap in PostCardContent.

### F5. Empty states (FADE/Quick + scale)
All empty states (connection error, filter empty, search no results, normal empty) wrapped in `AnimatedVisibility` with `fadeIn + scaleIn(0.9f)`.

### F6. Queue status chip (BOUNCE/Bouncy)
- UPLOADING: `rememberInfiniteTransition` pulse alpha 0.6→1.0
- Status change: bounce scale + `animateColorAsState`
- FAILED: shake via `Animatable` offset oscillation (-4dp→4dp→0dp, 3 cycles)

### F7. Snackbar (SLIDE/Snappy)
Both error snackbar (line 578) and pin confirmation snackbar (line 562) — `AnimatedVisibility` with `slideInVertically` + overshoot.

### F8. Search bar (SLIDE/Quick) — NEW
`SearchTopBar` slides in from top with fade when search activates. Reverse on close.
- `AnimatedVisibility` wrapping the `if (isSearchActive)` branch (line 157)
- `slideInVertically(initialOffsetY = { -it }) + fadeIn` / reverse

### F9. Filter chips bar (FADE/Quick) — NEW
`FilterChipsBar` fades in/out when toggling between search and normal mode.
- Already has `animateColorAsState` for chip selection (line 715)
- Add `AnimatedVisibility` wrapper for the `if (!isSearchActive)` block (line 263)

### F10. Account switcher items (SLIDE/Gentle) — NEW
Inside `AccountSwitcherBottomSheet` (line 954), account `ListItem`s get staggered slide-in.
- ModalBottomSheet has built-in slide animation
- Add stagger pattern to account items inside the sheet

### F11. Pinned section header (FADE/Quick) — NEW
"📌 Pinned" header at line 790 — `AnimatedVisibility` with `fadeIn + slideInVertically` when pinned posts exist.

### F12. Account switch overlay (FADE/Quick) — NEW
"Switching account..." overlay (line 288) — crossfade entrance with scale on the spinner.

---

## Composer Screen — 9 animations

### C1. Image grid preview (SCALE/Bouncy)
`ImageGridPreview` (line 556) — each image thumbnail scales in with bounce when added. Scale-out on removal.
- `AnimatedVisibility` per grid item with `scaleIn` using `Bouncy`

### C2. Link preview card (SLIDE/Gentle)
After link loads, card slides up + fades in. Loading: `PulsingPlaceholder` replaces `LinearProgressIndicator` (line 317).

### C3. Schedule chip (SPRING/Bouncy)
Chip at line 365 pops in with `scaleIn` using `Bouncy` when `state.scheduledAt != null`.

### C4. Publish button (BOUNCE/BouncyQuick)
Button at line 421 — bounce on click, loading pulse during submit, `AnimatedContent` icon swap to checkmark on success.

### C5. Character counter color (FADE/Quick)
Three-tier `animateColorAsState`: onSurfaceVariant → tertiary (>280) → error (>500). Matches current logic at line 211.

### C6. Action buttons staggered (SCALE/Gentle)
Publish button (line 421) + Row of [Draft, Schedule] (line 433). Two-step stagger: button first, then row.

### C7. Error text (SLIDE/Snappy)
Error at line 407 — `AnimatedVisibility` with `slideInHorizontally + fadeIn`.

### C8. Hashtag chips (SPRING/Bouncy) — NEW
Extracted tags FlowRow (line 225) — staggered `scaleIn` per chip as tags appear/change.

### C9. Edit/Preview crossfade (FADE/Quick) — NEW
`Crossfade(targetState = state.isPreviewMode)` wrapping the `if (state.isPreviewMode)` branch (line 167). Smooth transition between edit and preview modes.

---

## Detail Screen — 5 animations

### D1. Content reveal (FADE/Gentle)
Sequential: status row → text → tags → image gallery → link preview → PostStatsSection. 80ms delay each.
- Stagger pattern with `AnimatedVisibility` per section
- PostStatsSection (line 307) already has internal `AnimatedVisibility` for expand/collapse

### D2. Status badge entrance (SPRING/Bouncy)
First in reveal sequence (index 0). Scale-in with bounce.

### D3. Delete dialog (SCALE/Snappy)
Replace `AlertDialog` at line 312 with `AnimatedDialog` wrapper.

### D4. PostStatsSection (SLIDE/Gentle)
Last in reveal sequence. Slides up. Internal expand/collapse already animated.

### D5. Pin toggle feedback (BOUNCE/Bouncy) — NEW
Pin icon in TopAppBar (line 78 area) — bouncy scale pulse when toggling featured state. `animateFloatAsState` scale 1→1.2→1.

---

## Settings Screen — 3 animations

### S1. Account card (FADE/Quick) — NEW
Current account Card (line 78) — fade-in with subtle scale on screen entry.

### S2. Disconnect confirmation dialog (SCALE/Snappy)
**New behavior:** Add confirmation dialog before both "Disconnect Current Account" (line 139) and "Disconnect All Accounts" (line 165). Uses `AnimatedDialog` wrapper.

### S3. Theme chip selection (SPRING/Bouncy) — NEW
In `ThemeModeSelector` (line 184), selected chip gets a brief bouncy scale pulse on selection change.

---

## Stats Screen — 3 animations (all NEW)

### ST1. Stats cards staggered entrance (SCALE/Bouncy)
Four `StatsCard` composables (lines 68-98) — staggered scale-in with bounce. 50ms delay per card.

### ST2. Writing stats reveal (SLIDE/Gentle)
`OutlinedCard` at line 109 — slide-up with fade after cards complete. Internal `WritingStatRow`s cascade.

### ST3. Number count-up (FADE/Quick)
Stat values animate from 0 to target via `animateIntAsState`. Subtle but adds life to the dashboard.

---

## Navigation Transitions — 8 routes

Each `composable()` receives `enterTransition`, `exitTransition`, `popEnterTransition`, `popExitTransition`.

| Route | Enter | Pop Exit |
|-------|-------|----------|
| Setup | fadeIn(500ms) | — (inclusive popUpTo) |
| Feed | fadeIn(300ms) | fadeOut(200ms) |
| Composer | slideInVertically + fadeIn | slideOutVertically + fadeOut |
| Detail | slideInHorizontally + fadeIn | slideOutHorizontally + fadeOut |
| Settings | slideInHorizontally | slideOutHorizontally |
| Stats | slideInHorizontally | slideOutHorizontally |
| Preview | slideInVertically + fadeIn | slideOutVertically + fadeOut |
| AddAccount | slideInVertically + fadeIn | slideOutVertically + fadeOut |

---

## File Structure

```
ui/
├── animation/
│   └── SwooshMotion.kt          # Shared specs + reduced motion
├── components/
│   ├── AnimatedDialog.kt        # Scale-in dialog wrapper
│   └── PulsingPlaceholder.kt    # Pulsing loading placeholder
├── feed/
│   └── FeedScreen.kt            # F1-F12
├── composer/
│   └── ComposerScreen.kt        # C1-C9
├── detail/
│   └── DetailScreen.kt          # D1-D5
├── settings/
│   └── SettingsScreen.kt        # S1-S3
├── stats/
│   └── StatsScreen.kt           # ST1-ST3
└── navigation/
    └── NavGraph.kt              # 8 route transitions
```

**New files:** 3 (`SwooshMotion.kt`, `AnimatedDialog.kt`, `PulsingPlaceholder.kt`)
**Modified files:** 6 (`FeedScreen.kt`, `ComposerScreen.kt`, `DetailScreen.kt`, `SettingsScreen.kt`, `StatsScreen.kt`, `NavGraph.kt`)

## Testing Strategy

- Existing unit tests pass unchanged (animations don't affect business logic)
- Manual verification on emulator per animation
- Test with "Remove animations" accessibility setting for reduced-motion fallback
