# District Companion Lottie handoff

This folder contains one standalone Bodymovin/Lottie JSON animation for every visible District Companion state. The assets animate the companion face and its state signals only; the app owns the reducer, microphone, speech, model, navigation, and action services.

## Contents

`lottie-manifest.json` is the canonical index. It records each file, duration, frame count, loop behavior, reduced-motion frame, contract mapping, palette, and native color keypaths.

- Conversation: Waking, Ready, Listening, Acknowledged, Thinking, Clarification, Awaiting confirmation, Taking action, Speaking, and Completed.
- Activity variant: Searching, selected when `conversationState == thinking` and `activityKind == searching`.
- Surface variant: Resting, shown as the Sleeping pose before the orb is activated.
- Independent conditions: Mic off, No permission, and Completion residue.

The set contains 15 files in `lottie/`. Every composition is 240 × 240 at 60 fps and has a duration of at least 2,000 ms. It has no fonts, images, masks, effects, expressions, gradients, or external assets.

## Playback contract

Use the manifest's `loop` value rather than guessing from the filename. Waking, Acknowledged, and Completed are finite transitions. Resting and Mic off are static compositions. Continuous activity states loop until the app reducer changes state.

For Reduce Motion, render `reducedMotionFrame` as a still image. Do not repeatedly play a shortened loop. On a state change, replace the animation immediately; do not wait for the prior loop to finish. Completion plays once for 2 seconds, then the app returns to Ready and may show Completion residue until the next input.

Condition precedence should be:

1. No permission
2. Mic off
3. Conversation state or activity variant
4. Completion residue, only after Completed has returned to Ready

Resting is a surface pose, not a conversation state. Searching is a visual variant of Thinking, not a second reducer state.

## Theme colors

The JSON ships with the dark-theme palette. Native apps may replace colors without editing geometry by using the keypaths in `nativeColorKeypaths`:

- `**.Theme Body Fill.Color`
- `**.Theme Eye Fill.Color`
- `**.Theme Signal Fill.Color`
- `**.Theme Signal Stroke.Color`
- `**.Theme Success Fill.Color`
- `**.Theme Success Stroke.Color`
- `**.Theme Unavailable Fill.Color`

The manifest contains both approved dark and light palettes. Missing keypaths are expected when a particular animation does not draw that semantic color. Mic off deliberately uses the unavailable fill for both body and eyes; do not replace that keypath with the active body or eye colors.

## Swift / Lottie iOS example

Add the JSON files to the app bundle while preserving their names. With Lottie 4.x, a host view can select the file from the manifest-derived state mapping:

```swift
import Lottie

let animation = LottieAnimation.named("district-companion-listening.lottie")
let view = LottieAnimationView(animation: animation)
view.loopMode = .loop
view.contentMode = .scaleAspectFit

let bodyKeypath = AnimationKeypath(keypath: "**.Theme Body Fill.Color")
let bodyColor = LottieColor(r: 0.310, g: 0.145, b: 0.616, a: 1)
view.setValueProvider(ColorValueProvider(bodyColor),
                      keypath: bodyKeypath)
view.play()
```

Use the exact API supported by the Lottie version pinned in the native app. Android should apply the same manifest mapping, loop rules, reduced-motion still frame, and semantic palette with its Lottie dynamic-property APIs.

## Regeneration

The files are deterministically generated from the prototype's canonical face geometry:

```bash
node handoffs/district-companion/scripts/generate-lottie.mjs
```

Regenerate after changing `src/components/DistrictCompanion/districtCompanionStates.js`, then run `npm run docs:lint` to verify that every runtime state still has a matching asset.
