# Neutral icon motion

## Motion character

**Exact cycles. Quiet amplitude. Finite feedback.**

Neutral motion communicates that the platform is doing something; it never decorates an otherwise static interface. The icon's construction remains stable. Continuous movement is permitted only while a real process is active, and completion feedback runs once.

This follows Apple's guidance to make motion purposeful, brief and precise, and to provide an alternative for people who reduce motion:

- [Apple Human Interface Guidelines: Motion](https://developer.apple.com/design/human-interface-guidelines/motion)
- [Apple Human Interface Guidelines: Accessibility](https://developer.apple.com/design/human-interface-guidelines/accessibility)
- [Apple reduced-motion evaluation criteria](https://developer.apple.com/help/app-store-connect/manage-app-accessibility/reduced-motion-evaluation-criteria)

## Profiles

| Profile | Duration | Behaviour | Activation |
| --- | ---: | --- | --- |
| `running` | `1200ms` | Even clockwise rotation | While execution is active |
| `retrying` | `1000ms` | Deliberate clockwise rotation | While another attempt is active |
| `syncing` | `1400ms` | Quiet clockwise rotation | While synchronization is active |
| `rolling-back` | `1100ms` | Counterclockwise rotation | While reversal is active |
| `detecting` | `1800ms` | Slow radar rotation | While signal evaluation is active |
| `investigating` | `1600ms` | One-pixel vertical scan overlay | While investigation is active |
| `monitoring` | `2200ms` | Two-pixel leading-to-trailing sample | While monitoring is active |
| `success-enter` | `160ms` | Single confirmation reveal | Once when success appears |
| `resolution-enter` | `180ms` | Single final confirmation | Once when resolution appears |
| `failure-enter` | `140ms` | Single contrast reveal | Once when failure appears |

The machine-readable profiles and icon assignments are in [`manifest/motion.json`](manifest/motion.json).

## Rules

- Motion is an extension, not part of the SVG file. Use the canonical static asset inside `.neutral-icon-motion`.
- Add `data-motion` only while its semantic trigger is true. When the state changes, end it through the phase-aware controller so the motion reaches a deliberate handoff pose instead of snapping off.
- Rotations use linear timing so the loop seam never accelerates or stutters.
- Scan and monitoring overlays use a restrained standard easing and never move the base glyph.
- Completion and failure feedback runs once. Never loop success, resolution or failure.
- No bounce, spring, wobble, shake, blur, depth or multi-axis movement.
- No layout-changing animation. Only `transform` and `opacity` animate.
- Do not animate queued, pending, paused, blocked, healthy, degraded, at-risk or static telemetry icons.
- Never use motion as the only state indicator; retain the icon, label and semantic colour.
- A page should not intentionally attract attention to many continuous indicators at once. Prefer one animated group-level indicator over dozens of identical animated rows.
- Virtualized or hidden content should remove `data-motion` or set `data-motion-paused="true"`.

## Natural stopping and linked states

Removing a looping CSS animation directly can freeze or snap it at an arbitrary angle. Product implementations should therefore use [`runtime/motion-controller.mjs`](runtime/motion-controller.mjs) whenever an animated state can transition into another state.

The controller applies this choreography:

1. A rotating glyph decelerates to its next `90°` cardinal pose in `120–300ms`.
2. The outgoing state fades for `72ms` at that deliberate pose.
3. The glyph is replaced while fully transparent.
4. The next continuous state fades in over `108ms`, already moving in its own direction.
5. A terminal state starts its one-shot confirmation instead of receiving an additional entrance animation.

Scan and monitoring overlays keep the base glyph fixed and exit through the same short opacity handoff. A clockwise-to-counterclockwise transition therefore never reverses instantaneously: it settles first, then begins the linked reversal from rest.

The total handoff is deliberately bounded. It preserves continuity without making a person wait for a full revolution or a multi-second scan cycle to finish. The `90°` stops are transitional poses; a final static icon resets to its canonical orientation while transparent.

This applies Apple's principle to “gracefully transition motion to a resting state” rather than stopping abruptly. See [Apple Human Interface Guidelines: Always On](https://developer.apple.com/design/human-interface-guidelines/always-on).

## Markup

```html
<span
  class="neutral-icon-motion"
  data-icon-size="20"
  data-motion="running"
  role="img"
  aria-label="Running"
>
  <!-- Inline SVG or image using state-running-20.svg -->
</span>
```

Use the phase-aware controller when the state can change:

```js
import { createNeutralIconMotionController } from "./generated/index.mjs";

const controller = createNeutralIconMotionController(iconWrapper);

await controller.transitionTo("investigating", {
  replace: () => {
    iconWrapper.innerHTML = investigatingIconSvg;
  },
});
```

`replace` runs only after the outgoing motion reaches its rest pose and fades out. Calls are serialized so rapid state updates cannot overlap animations.

For a decorative icon inside a labelled status component, keep the wrapper hidden from assistive technology and put the accessible state name on the enclosing component.

## Reduced motion

Under `prefers-reduced-motion: reduce`, every continuous and one-shot icon animation is disabled. The final static glyph remains visible, so meaning is preserved without spinning, scanning, scaling or flashing.
