{
  "$description": "Cosmos 2.4.0 named motion motifs: the trigger, steps, allowed properties, duration, easing and per-tier and per-platform behaviour of every state change the system animates, with the sampled curve values a native port reproduces.",
  "version": "2.4.0",
  "updated": "2026-10-03",
  "status": "Normative specification",
  "fetchWhen": [
    "animating a state change, on any platform",
    "choosing between the tiers, or deciding whether a change animates at all",
    "porting a transition to a native toolkit and proving curve parity"
  ],
  "properties": ["transform", "opacity"],
  "durations": {
    "feedback": {
      "$type": "duration",
      "$value": { "value": 150, "unit": "ms" }
    },
    "state": {
      "$type": "duration",
      "$value": { "value": 300, "unit": "ms" }
    },
    "context": {
      "$type": "duration",
      "$value": { "value": 500, "unit": "ms" }
    }
  },
  "easingPurposes": {
    "standard": {
      "label": "Standard — in-place transform and state",
      "web": {
        "$type": "cubicBezier",
        "$value": [0.2, 0, 0.2, 1]
      },
      "native": {
        "$type": "cubicBezier",
        "$value": [0.2, 0, 0.2, 1]
      }
    },
    "arrival": {
      "label": "Arrival — emphasis and entrance",
      "web": {
        "$type": "cubicBezier",
        "$value": [0.22, 1, 0.36, 1]
      },
      "native": {
        "$type": "cubicBezier",
        "$value": [0.05, 0.7, 0.1, 1]
      }
    },
    "departure": {
      "label": "Departure — exit",
      "web": {
        "$type": "cubicBezier",
        "$value": [0.4, 0, 0.7, 0.2]
      },
      "native": {
        "$type": "cubicBezier",
        "$value": [0.3, 0, 0.8, 0.15]
      }
    },
    "transform": {
      "label": "Transform only",
      "web": null,
      "native": {
        "$type": "cubicBezier",
        "$value": [0.2, 0, 0, 1]
      }
    }
  },
  "tiers": ["full", "reduced", "static"],
  "motifs": [
    {
      "id": "targetAcquired",
      "job": "Tell the person that the thing they were tracking is now the thing they have.",
      "trigger": "A selection, fix or search resolves on a target the person was already watching.",
      "steps": ["The glow scales past its resting size and settles back while its opacity resolves.", "The ring resolves to its final angle with the glow."],
      "properties": ["transform", "opacity"],
      "duration": "state",
      "easing": ["standard", "arrival"],
      "tiers": {
        "full": "Glow scale and ring angle both travel over the state duration.",
        "reduced": "Opacity only, over the feedback duration; the ring appears already at its final angle.",
        "static": "Glow and ring settle at once."
      },
      "platforms": {
        "css": "One keyframe set on a wrapper: transform for scale and angle, opacity for the glow.",
        "compose": "A single graphicsLayer animating scale and rotation; the glow is a separate layer so the two can differ in duration.",
        "wear": "The same two layers. Round geometry carries the ring; the arc length is the measured one, not a web constant."
      },
      "settlesTo": "The ring at its final angle and the glow at its resting opacity."
    },
    {
      "id": "listReveal",
      "job": "Show that a set arrived, in an order a person can follow down.",
      "trigger": "A list is populated for the first time, filtered, or reordered.",
      "steps": [
        "Items enter along an arc, each offset from the one above it.",
        "Each item resolves to opacity as it reaches its row.",
        "The last item resolves; no item animates after that."
      ],
      "properties": ["transform", "opacity"],
      "duration": "feedback",
      "easing": ["arrival"],
      "tiers": {
        "full": "Items travel the arc, staggered by a fraction of the feedback duration each, within one feedback budget in total.",
        "reduced": "Opacity only, staggered, over the feedback duration.",
        "static": "The list is present at once, in its final order."
      },
      "platforms": {
        "css": "One animation-delay per index on a transform and opacity pair; the stagger is a custom property, never a per-row literal.",
        "compose": "A staggered reveal on the item layout; a LazyColumn does not animate its own content and must not be asked to.",
        "wear": "Never used on a watch list. A round screen has no arc to reveal along, and scrolling competes with the reveal for the same gesture."
      },
      "settlesTo": "Every item at its row, at full opacity, in the final order."
    },
    {
      "id": "horizonSweep",
      "job": "Carry time or progress across a wide surface without a spinner.",
      "trigger": "A timeline, countdown or long-running task advances to a new reading.",
      "steps": ["A band translates across the horizon gradient, leaving the previous reading behind it.", "The band resolves at the position the stored reading gives it."],
      "properties": ["transform", "opacity"],
      "duration": "context",
      "easing": ["standard"],
      "tiers": {
        "full": "The band travels over the context duration.",
        "reduced": "The band is placed at its final position and its opacity resolves over the feedback duration; no travel.",
        "static": "The band is placed at its final position."
      },
      "platforms": {
        "css": "A gradient overlay inside overflow:hidden, moved with transform so the horizon itself is never repainted.",
        "compose": "A translated overlay layer; the gradient is a brush on the parent, not an animated shader.",
        "wear": "Never used on a watch. There is no horizon band wide enough to sweep, and a travelling element on a round screen reads as a second hand."
      },
      "settlesTo": "The band at the position of the stored reading, with the earlier reading no longer drawn."
    },
    {
      "id": "spineAdvance",
      "job": "Move context forward one step, so the person always knows which step they are on.",
      "trigger": "The context spine advances from the current step to the next one.",
      "steps": ["The outgoing step resolves to inactive.", "The incoming step resolves to active.", "The connector between them fills to its new length."],
      "properties": ["transform", "opacity"],
      "duration": "state",
      "easing": ["standard"],
      "tiers": {
        "full": "The outgoing and incoming steps cross-resolve and the connector grows over the state duration.",
        "reduced": "The connector is placed at its final length and the two steps cross-resolve in opacity over the feedback duration.",
        "static": "The step and the connector are set together, with no cross-resolve."
      },
      "platforms": {
        "css": "Transform on the connector so growth is a scale rather than a width; opacity on the two labels.",
        "compose": "A scaleX on the connector inside a fixed-width track; labels cross-fade.",
        "wear": "One step at a time. The spine is the round bezel: the connector is the bezel arc and only the active step label changes."
      },
      "settlesTo": "The incoming step active, the outgoing step inactive, the connector at its final length."
    },
    {
      "id": "sheetRise",
      "job": "Show which context a layer came from, and leave it dismissible.",
      "trigger": "A sheet, dialog or popover is presented over the current context.",
      "steps": [
        "The scrim resolves its opacity.",
        "The surface travels in from the edge it is anchored to.",
        "The drag or close affordance resolves with the surface, never after it."
      ],
      "properties": ["transform", "opacity"],
      "duration": "state",
      "easing": ["arrival", "departure"],
      "tiers": {
        "full": "Scrim and surface travel together over the state duration; dismissal uses the departure purpose.",
        "reduced": "The scrim resolves over the feedback duration and the surface is presented at its resting offset.",
        "static": "The surface is presented at its resting offset with the scrim at its resting opacity. It is still a dismissible layer with a real close target."
      },
      "platforms": {
        "css": "A dialog element and its backdrop, transformed rather than resized, with the surface kept inside the viewport on every breakpoint.",
        "compose": "A ModalBottomSheet or Dialog; the sheet does its own scrim and must not be given a second one.",
        "wear": "A single full-bleed layer. A bottom sheet on a round screen covers the bezel, so the watch presents its surface as a replacement view with a back target instead."
      },
      "settlesTo": "The surface at its resting offset, the scrim at its resting opacity, dismissal still available."
    },
    {
      "id": "dataStale",
      "job": "Say that a reading is no longer fresh, without asking for attention.",
      "trigger": "A stored reading falls outside its freshness window.",
      "steps": ["The reading resolves to the dimmed treatment.", "The stale label appears with the reading, and is the only new element."],
      "properties": ["opacity"],
      "duration": "feedback",
      "easing": ["standard"],
      "tiers": {
        "full": "The dim resolves over the feedback duration. It runs once.",
        "reduced": "The dim resolves over the feedback duration, opacity only, once.",
        "static": "The reading is presented dimmed with its label."
      },
      "platforms": {
        "css": "An opacity step on the reading, plus a class or attribute that reveals the label. The label is never cross-faded into place.",
        "compose": "An alpha change on the reading container, and the label as a sibling so its content is present for assistive technology from the first frame.",
        "wear": "Dimming only. A watch reading carries no label, because there is no room for one beside a value; the staleness is stated in the detail view instead."
      },
      "settlesTo": "The reading dimmed, with its stale label present.",
      "constraints": [
        "Never a pulse, a blink, a loop or a repeat. A reading that has gone stale has not become more urgent, and attention-pulsing a stored value inverts C21.",
        "The dim is opacity on the reading. The label is inserted, not animated, so a stale reading is never briefly readable as a fresh one."
      ]
    },
    {
      "id": "routeChange",
      "job": "Carry the reader from one context to the next without losing the frame.",
      "trigger": "A route change resolves to a different context.",
      "steps": [
        "The outgoing context resolves out on the departure purpose.",
        "The incoming context arrives on the arrival purpose.",
        "Any shared spine advances on its own motif, not on this timeline."
      ],
      "properties": ["transform", "opacity"],
      "duration": "context",
      "easing": ["arrival", "departure"],
      "tiers": {
        "full": "Outgoing and incoming run on the context duration, in opposite directions.",
        "reduced": "Both resolve in opacity over the feedback duration, with no travel.",
        "static": "The incoming context is presented at once."
      },
      "platforms": {
        "css": "Two cross-resolving layers inside a stable frame; the route does not scroll, and a back navigation runs the same motif in reverse.",
        "compose": "A shared NavHost frame whose outgoing and incoming composables cross-resolve; the host owns the duration so a screen cannot pick its own.",
        "wear": "A replacement view with the back target always visible. Cross-fading two contexts on a round screen hides the bezel that carries the gesture."
      },
      "settlesTo": "The incoming context at rest, the outgoing one gone, focus and scroll at the incoming context own values."
    }
  ],
  "parityFixture": {
    "algorithm": [
      "For each published curve, sample x from 0 to 1 in equal steps of 1 / (count - 1).",
      "x(t) = 3(1-t)²t·x1 + 3(1-t)t²·x2 + t³, and the same form with y1, y2 for y(t). P0 is (0,0) and P3 is (1,1), so their terms drop out.",
      "Bisect t on [0, 1] a fixed number of iterations, keeping the half where x(t) is below x, and evaluate y(t) at the final midpoint.",
      "Publish y to six decimal places. The endpoints are 0 and 1 exactly; every sample is monotonic non-decreasing."
    ],
    "sampleCount": 21,
    "bisectionSteps": 64,
    "durations": { "feedback": 150, "state": 300, "context": 500 },
    "curves": {
      "webPage": {
        "standard": {
          "curve": [0.2, 0, 0.2, 1],
          "samples": [
            0,
            0.022967,
            0.096127,
            0.208605,
            0.332657,
            0.447997,
            0.548019,
            0.632737,
            0.704011,
            0.76387,
            0.814074,
            0.856065,
            0.891,
            0.919815,
            0.94327,
            0.961987,
            0.976484,
            0.987193,
            0.994481,
            0.99866,
            1
          ]
        },
        "emphasis": {
          "curve": [0.22, 1, 0.36, 1],
          "samples": [
            0,
            0.215033,
            0.401097,
            0.554011,
            0.673978,
            0.764865,
            0.832192,
            0.881423,
            0.917147,
            0.942916,
            0.961383,
            0.974493,
            0.983675,
            0.989982,
            0.994194,
            0.996894,
            0.998524,
            0.99942,
            0.99984,
            0.999981,
            1
          ]
        },
        "entrance": {
          "curve": [0.16, 0.84, 0.44, 1],
          "samples": [
            0,
            0.22536,
            0.393474,
            0.522058,
            0.622232,
            0.701332,
            0.764394,
            0.815,
            0.855766,
            0.888653,
            0.915157,
            0.936443,
            0.953432,
            0.966859,
            0.97732,
            0.985302,
            0.991207,
            0.99537,
            0.998071,
            0.999547,
            1
          ]
        },
        "exit": {
          "curve": [0.4, 0, 0.7, 0.2],
          "samples": [
            0,
            0.001093,
            0.004591,
            0.01084,
            0.020219,
            0.033137,
            0.050037,
            0.071391,
            0.0977,
            0.129493,
            0.167324,
            0.21176,
            0.263382,
            0.322771,
            0.390501,
            0.467123,
            0.553159,
            0.649083,
            0.755313,
            0.872196,
            1
          ]
        }
      },
      "webApp": {
        "standard": {
          "curve": [0.2, 0, 0.2, 1],
          "samples": [
            0,
            0.022967,
            0.096127,
            0.208605,
            0.332657,
            0.447997,
            0.548019,
            0.632737,
            0.704011,
            0.76387,
            0.814074,
            0.856065,
            0.891,
            0.919815,
            0.94327,
            0.961987,
            0.976484,
            0.987193,
            0.994481,
            0.99866,
            1
          ]
        },
        "emphasis": {
          "curve": [0.22, 1, 0.36, 1],
          "samples": [
            0,
            0.215033,
            0.401097,
            0.554011,
            0.673978,
            0.764865,
            0.832192,
            0.881423,
            0.917147,
            0.942916,
            0.961383,
            0.974493,
            0.983675,
            0.989982,
            0.994194,
            0.996894,
            0.998524,
            0.99942,
            0.99984,
            0.999981,
            1
          ]
        },
        "entrance": {
          "curve": [0.16, 0.84, 0.44, 1],
          "samples": [
            0,
            0.22536,
            0.393474,
            0.522058,
            0.622232,
            0.701332,
            0.764394,
            0.815,
            0.855766,
            0.888653,
            0.915157,
            0.936443,
            0.953432,
            0.966859,
            0.97732,
            0.985302,
            0.991207,
            0.99537,
            0.998071,
            0.999547,
            1
          ]
        },
        "exit": {
          "curve": [0.4, 0, 0.7, 0.2],
          "samples": [
            0,
            0.001093,
            0.004591,
            0.01084,
            0.020219,
            0.033137,
            0.050037,
            0.071391,
            0.0977,
            0.129493,
            0.167324,
            0.21176,
            0.263382,
            0.322771,
            0.390501,
            0.467123,
            0.553159,
            0.649083,
            0.755313,
            0.872196,
            1
          ]
        }
      },
      "android": {
        "standard": {
          "curve": [0.2, 0, 0.2, 1],
          "samples": [
            0,
            0.022967,
            0.096127,
            0.208605,
            0.332657,
            0.447997,
            0.548019,
            0.632737,
            0.704011,
            0.76387,
            0.814074,
            0.856065,
            0.891,
            0.919815,
            0.94327,
            0.961987,
            0.976484,
            0.987193,
            0.994481,
            0.99866,
            1
          ]
        },
        "enter": {
          "curve": [0.05, 0.7, 0.1, 1],
          "samples": [
            0,
            0.4477,
            0.621384,
            0.719129,
            0.784319,
            0.83153,
            0.867367,
            0.895379,
            0.917687,
            0.935665,
            0.950247,
            0.962103,
            0.971723,
            0.979481,
            0.985667,
            0.990511,
            0.994196,
            0.996873,
            0.998666,
            0.999679,
            1
          ]
        },
        "exit": {
          "curve": [0.3, 0, 0.8, 0.15],
          "samples": [
            0,
            0.001384,
            0.005538,
            0.012512,
            0.0224,
            0.035343,
            0.051521,
            0.071163,
            0.094548,
            0.122019,
            0.153998,
            0.191007,
            0.233698,
            0.282906,
            0.339716,
            0.405586,
            0.482544,
            0.573561,
            0.683301,
            0.819872,
            1
          ]
        },
        "transform": {
          "curve": [0.2, 0, 0, 1],
          "samples": [
            0,
            0.028533,
            0.15625,
            0.350586,
            0.5,
            0.60722,
            0.688067,
            0.751414,
            0.802284,
            0.84375,
            0.877834,
            0.905936,
            0.929072,
            0.948004,
            0.963318,
            0.97548,
            0.984862,
            0.99177,
            0.996459,
            0.999142,
            1
          ]
        }
      },
      "watch": {
        "standard": {
          "curve": [0.2, 0, 0.2, 1],
          "samples": [
            0,
            0.022967,
            0.096127,
            0.208605,
            0.332657,
            0.447997,
            0.548019,
            0.632737,
            0.704011,
            0.76387,
            0.814074,
            0.856065,
            0.891,
            0.919815,
            0.94327,
            0.961987,
            0.976484,
            0.987193,
            0.994481,
            0.99866,
            1
          ]
        },
        "enter": {
          "curve": [0.05, 0.7, 0.1, 1],
          "samples": [
            0,
            0.4477,
            0.621384,
            0.719129,
            0.784319,
            0.83153,
            0.867367,
            0.895379,
            0.917687,
            0.935665,
            0.950247,
            0.962103,
            0.971723,
            0.979481,
            0.985667,
            0.990511,
            0.994196,
            0.996873,
            0.998666,
            0.999679,
            1
          ]
        },
        "exit": {
          "curve": [0.3, 0, 0.8, 0.15],
          "samples": [
            0,
            0.001384,
            0.005538,
            0.012512,
            0.0224,
            0.035343,
            0.051521,
            0.071163,
            0.094548,
            0.122019,
            0.153998,
            0.191007,
            0.233698,
            0.282906,
            0.339716,
            0.405586,
            0.482544,
            0.573561,
            0.683301,
            0.819872,
            1
          ]
        },
        "transform": {
          "curve": [0.2, 0, 0, 1],
          "samples": [
            0,
            0.028533,
            0.15625,
            0.350586,
            0.5,
            0.60722,
            0.688067,
            0.751414,
            0.802284,
            0.84375,
            0.877834,
            0.905936,
            0.929072,
            0.948004,
            0.963318,
            0.97548,
            0.984862,
            0.99177,
            0.996459,
            0.999142,
            1
          ]
        }
      }
    },
    "timing": [
      {
        "id": "targetAcquired",
        "duration": "state",
        "easing": {
          "web": ["standard", "emphasis", "entrance"],
          "native": ["standard", "enter"]
        },
        "properties": ["transform", "opacity"],
        "settlesTo": "The ring at its final angle and the glow at its resting opacity."
      },
      {
        "id": "listReveal",
        "duration": "feedback",
        "easing": {
          "web": ["emphasis", "entrance"],
          "native": ["enter"]
        },
        "properties": ["transform", "opacity"],
        "settlesTo": "Every item at its row, at full opacity, in the final order."
      },
      {
        "id": "horizonSweep",
        "duration": "context",
        "easing": {
          "web": ["standard"],
          "native": ["standard"]
        },
        "properties": ["transform", "opacity"],
        "settlesTo": "The band at the position of the stored reading, with the earlier reading no longer drawn."
      },
      {
        "id": "spineAdvance",
        "duration": "state",
        "easing": {
          "web": ["standard"],
          "native": ["standard"]
        },
        "properties": ["transform", "opacity"],
        "settlesTo": "The incoming step active, the outgoing step inactive, the connector at its final length."
      },
      {
        "id": "sheetRise",
        "duration": "state",
        "easing": {
          "web": ["emphasis", "entrance", "exit"],
          "native": ["enter", "exit"]
        },
        "properties": ["transform", "opacity"],
        "settlesTo": "The surface at its resting offset, the scrim at its resting opacity, dismissal still available."
      },
      {
        "id": "dataStale",
        "duration": "feedback",
        "easing": {
          "web": ["standard"],
          "native": ["standard"]
        },
        "properties": ["opacity"],
        "settlesTo": "The reading dimmed, with its stale label present."
      },
      {
        "id": "routeChange",
        "duration": "context",
        "easing": {
          "web": ["emphasis", "entrance", "exit"],
          "native": ["enter", "exit"]
        },
        "properties": ["transform", "opacity"],
        "settlesTo": "The incoming context at rest, the outgoing one gone, focus and scroll at the incoming context own values."
      }
    ]
  }
}
