{
  "name": "WaitingIndicator",
  "type": "pattern",
  "description": "Eight squares whose opacity ramps in sequence, for a turn-waiting state on a voice call",
  "export": "WaitingIndicator",
  "path": "@gtmi/strix-react/animations/waiting-indicator",
  "props": [
    {
      "name": "isWaiting",
      "type": "boolean",
      "default": "true",
      "description": "Whether to animate. False holds every square at its peak opacity"
    },
    {
      "name": "tone",
      "type": "'accent' | 'neutral' | 'destructive' | 'success' | 'waveform-agent' | 'waveform-transferred'",
      "default": "'accent'",
      "description": "Semantic square color, mapped to a bg-* token utility"
    },
    {
      "name": "peaks",
      "type": "'ramp' | 'uniform'",
      "default": "'ramp'",
      "description": "ramp gives each square its own step of the 10/20/…/100 opacity ramp (.agent-waiting, .user-waiting); uniform peaks all eight at 1 (.transfer-agent-animation, whose squares are binary)"
    },
    {
      "name": "align",
      "type": "'start' | 'end'",
      "default": "'end'",
      "description": "end sits the squares on the baseline (.agent-waiting), start hangs them from the top (.user-waiting)"
    },
    {
      "name": "decorative",
      "type": "boolean",
      "default": "true",
      "description": "Hide from assistive technology. False renders role=\"img\" with label as the accessible name"
    },
    {
      "name": "label",
      "type": "string",
      "default": "'Waiting'",
      "description": "Accessible name when decorative is false"
    }
  ],
  "parts": [
    { "name": "waiting-indicator", "description": "The flex row, via data-slot, carrying data-tone" },
    { "name": "waiting-indicator-square", "description": "One square, via data-slot, carrying data-index" }
  ],
  "ai_tags": [
    "waiting",
    "typing-indicator",
    "voice",
    "turn-taking",
    "opacity",
    "animation",
    "css-keyframes",
    "pattern"
  ],
  "since": "0.2.0",
  "source": "Figma zT3vGm4QlFEI2Jp9nxyFKI atoms .agent-waiting (22:4103) and .user-waiting (22:4185), read in Dev Mode",
  "tokens_used": [
    "--color-icon-accent",
    "--color-icon-primary",
    "--color-icon-destructive",
    "--color-icon-success",
    "--animate-waiting-square",
    "--spacing-gap-100"
  ],
  "notes": "WHY THIS EXISTS AT ALL, when both atoms were already mapped to AudioVisualizer. The two waiting atoms are EIGHT 8px SQUARES WHOSE OPACITY RAMPS 10/20/30/40/50/60/80/100 across nine frames, revealed left to right. AudioVisualizer is a SIXTEEN-BAR LEVEL METER whose bar HEIGHTS follow a live RMS through a requestAnimationFrame loop. Different geometry, different animated property, different meaning — waiting for a turn rather than speaking. The mapping to AudioVisualizer stood because all six Animations bindings were extracted over REST, and node opacity is one of the things AGENTS.md 8 records that route as dropping: the extractor saw nine identical frames of eight identical squares and concluded, reasonably, that the storyboard varied only in bar height and that the atom was the meter's storyboard. Dev Mode shows the opacity, and with it that the atom animates nothing else — the squares are 8x8 in every frame. This is the second time in this file that reading Dev Mode reversed a recorded mapping, after .icon and .cellphone-avatar. THE RAMP IS ASSERTED AS EXACT VALUES, not as monotonic: it skips 70, going 60 -> 80 -> 100, so a smooth eight-step ramp would satisfy a monotonic check while not being the thing Figma draws. ONE KEYFRAME SERVES ALL EIGHT SQUARES. --animate-waiting-square animates from 0 to var(--waiting-peak), and each square sets --waiting-peak to its own step, so the ramp survives the animation; without that variable the keyframe's own opacity would override the inline value and flatten all eight squares to the same peak. The travelling reveal is a NEGATIVE animation-delay one step (150ms) apart per square, so there is no per-square keyframe, no JavaScript, and the loop is already mid-cycle at mount rather than starting from empty. CSS RATHER THAN rAF IS THE POINT for reduced motion: tokens/motion.css caps animation-duration and iteration-count globally under prefers-reduced-motion, so this pattern needs no matchMedia listener and cannot forget one — unlike AudioVisualizer, which drives geometry from JavaScript and therefore has to track the preference itself. TONES are the same semantic scale AudioVisualizer uses, and for the same reason: the atoms fill their squares with the literal #ef223a and white, and a hex cannot follow a theme while white is invisible on a light page. All four clear WCAG 1.4.11's 3:1 as non-text graphics in both themes, proved with checkPairings over tokens DERIVED from the component's own cva output rather than named in the test, then mutation-tested — swapping accent for bg-highlight fails with 1.84:1 on the light page. Decorative by default, on the same argument as AudioVisualizer: whether a participant is waiting is application state and needs announcing from wherever that state lives, not from a graphic. No live region, and a test asserts none appears."
}
