[READ-ONLY] Mirror of https://github.com/bombshell-dev/tty. Platform independent 2D layout engine for terminal applications based on Clay
5

Configure Feed

Select the types of activity you want to include in your feed.

๐Ÿ“ add transitions design specification

Design spec for clayterm transitions: frame-snapshot-compatible interpolation
of element position, size, and color properties. Defines the deltaTime
convention, the animating signal on RenderResult, declarative enter/exit
semantics that replace Clay's function-pointer callbacks, and cancellation
as a structural consequence of re-describing state. Implementation is
gated on bumping the Clay submodule past the upstream transition commit.

Charles Lowell (Apr 21, 2026, 9:32 PM -0500) 3c5fee2c 9470772d

+658
+658
specs/transitions-spec.md
··· 1 + # Clayterm Transitions Specification 2 + 3 + **Version:** 0.1 (draft) **Status:** Design specification for a not-yet-implemented 4 + feature. Normative where it establishes invariants and contract. Descriptive 5 + where surfaces may settle during implementation. 6 + 7 + --- 8 + 9 + ## 1. Purpose 10 + 11 + A transition smoothly interpolates an element's visual properties over time. 12 + This specification defines how transitions integrate with Clayterm's frame-snapshot 13 + rendering model: how they are declared, how time is supplied, how enter and 14 + exit behaviors are expressed, and how callers observe in-flight animation so 15 + they can drive the render loop. 16 + 17 + Transitions are a first-class extension of the rendering contract defined in 18 + the [Clayterm Renderer Specification](renderer-spec.md). They do not change 19 + the architectural model, do not introduce a component tree, and do not require 20 + callers to hold cross-frame identity beyond the stable element identifiers they 21 + already use. 22 + 23 + --- 24 + 25 + ## 2. Scope 26 + 27 + ### In scope (normative) 28 + 29 + - The transition model and its relationship to the frame-snapshot rendering contract 30 + - Time handling and the `deltaTime` convention 31 + - The animating signal returned from `render()` 32 + - The declarative enter and exit model (no callbacks across the WASM boundary) 33 + - Element identity requirements for transitions 34 + - Cancellation semantics (as a consequence of the frame-snapshot model) 35 + 36 + ### In scope (non-normative, descriptive) 37 + 38 + - The shape of the `transition` field on the `open()` directive (shorthand and longhand) 39 + - The set of easing functions exposed in the initial surface 40 + - The wire encoding of transition data in the directive buffer 41 + - Interaction with line mode 42 + - Testing strategy 43 + 44 + ### Out of scope 45 + 46 + - Custom (JavaScript-authored) easing functions. Reserved for a future extension; 47 + the enum space is designed not to preclude them. 48 + - Proportional reversal (CSS-style dynamic shortening of duration when a 49 + transition is cancelled mid-flight). 50 + - Physics-based animation, spring interpolation, or keyframe sequences. 51 + - Any framework-level concept of "animation groups," "timelines," or choreography 52 + across multiple elements. Orchestration is a caller concern. 53 + - Input parsing (see [Input Specification](input-spec.md)). 54 + 55 + --- 56 + 57 + ## 3. Terminology 58 + 59 + **Transition.** A time-based interpolation of one or more of an element's 60 + visual properties between an initial value and a target value. 61 + 62 + **Transition property.** A specific visual attribute of an element that can be 63 + interpolated: position (x, y), size (width, height), background color, overlay 64 + color, border color, border width, or corner radius. 65 + 66 + **Easing.** A function mapping normalized progress in [0, 1] to an eased value 67 + in [0, 1]. Clayterm exposes a fixed set of built-in easings. 68 + 69 + **Transition state.** One of four modes an element can be in with respect to a 70 + given transition: idle, entering (element newly mounted), transitioning 71 + (property target changed on an existing element), or exiting (element removed 72 + from the tree but still being animated out). 73 + 74 + **Enter transition.** The animation played when an element first appears in the 75 + directive tree. Its initial state is derived from the element's target state 76 + by applying caller-supplied deltas (e.g., offset position, transparent color). 77 + 78 + **Exit transition.** The animation played when an element disappears from the 79 + directive tree. Its final state is derived from the element's last-seen state 80 + by applying caller-supplied deltas. The element is still rendered during its 81 + exit even though it is no longer in the directive tree. 82 + 83 + **Delta time (`deltaTime`).** The number of seconds elapsed since the previous 84 + render transaction. Used by the renderer to advance interpolation. 85 + 86 + **Animating signal.** A boolean flag in the render result indicating whether 87 + any transition is currently in progress. Callers use it to decide whether to 88 + schedule another frame. 89 + 90 + --- 91 + 92 + ## 4. Architectural Model 93 + 94 + _This section is normative._ 95 + 96 + ### 4.1 Relationship to the frame-snapshot model 97 + 98 + Transitions do not alter the frame-snapshot contract defined in INV-3 of the 99 + renderer specification. The directive array still fully describes the desired 100 + state for its frame. Transitions interpolate between the previous frame's 101 + state and the current frame's target state; they do not reintroduce a 102 + persistent component tree on the caller side. 103 + 104 + What transitions add is the requirement that element identifiers remain stable 105 + across frames for any element on which animation is desired. This is not a new 106 + invariant โ€” the existing pointer-event subsystem already relies on stable 107 + identifiers โ€” but it becomes load-bearing for transitions. 108 + 109 + ### 4.2 Time ownership 110 + 111 + The `Term` instance is the sole source of frame-to-frame time. On each 112 + `render()` call, the Term reads a monotonic clock and computes the elapsed 113 + seconds since the previous render. That value is passed to the layout engine 114 + to advance any in-flight transitions. 115 + 116 + The caller MAY override the computed delta via an explicit `deltaTime` option 117 + on `render()`. Use cases include deterministic testing, snapshot rendering, 118 + and compute-only renders where the caller is querying bounds without 119 + displaying output. 120 + 121 + The Term MUST NOT use a non-monotonic clock (e.g., `Date.now()`). Wall-clock 122 + time can move backward under NTP adjustments or DST, which would produce 123 + negative deltas and corrupt interpolation. 124 + 125 + ### 4.3 Delta clamping 126 + 127 + Clayterm does not clamp `deltaTime`. Long gaps between frames (process 128 + suspension, backgrounded terminal, debugger pause) produce large deltas. The 129 + underlying interpolation is duration-based and naturally clamps at 1.0 of 130 + progress, so a large delta causes in-flight transitions to complete rather 131 + than to overshoot or become unstable. 132 + 133 + This differs from physics-based engines, which clamp deltas to prevent 134 + tunneling. Transitions as specified here are not physics-based, so clamping 135 + is unnecessary. 136 + 137 + ### 4.4 Animation-loop signaling 138 + 139 + The render result MUST surface whether any transition is currently active. 140 + Callers use this signal to schedule the next frame. When no transition is 141 + active, callers may stop rendering until the next external event (input, 142 + resize, application state change). 143 + 144 + This requirement exists because terminal applications typically render 145 + on-demand rather than at a fixed refresh rate. Without an explicit animating 146 + signal, a caller has no way to know that a transition it triggered is still 147 + in progress. 148 + 149 + ### 4.5 Boundary preservation 150 + 151 + Transitions MUST NOT require function pointers, callbacks, or other 152 + non-serializable values to cross the TSโ†’WASM boundary. Easing and 153 + enter/exit initial-state computation are implemented on the C side using 154 + declarative configuration carried in the directive buffer. 155 + 156 + This preserves INV-2 (single transaction per frame): one binary buffer in, 157 + one result struct out. 158 + 159 + --- 160 + 161 + ## 5. Core Invariants 162 + 163 + _This section is normative._ 164 + 165 + **INV-T1. Time is driven by delta, not wall clock.** All transition 166 + interpolation advances by `deltaTime`, a per-frame seconds value. The 167 + renderer does not subscribe to an internal timer or schedule work of its own. 168 + 169 + **INV-T2. Render remains pure under time override.** When the caller supplies 170 + an explicit `deltaTime`, the render result depends only on the directive 171 + array, the previous frame's cell buffer, and the supplied `deltaTime`. This 172 + makes deterministic rendering possible for tests and snapshots. 173 + 174 + **INV-T3. No callbacks across the boundary.** Transition configuration MUST 175 + be fully serializable. No function pointers, closures, or callback registries 176 + cross the TSโ†’WASM boundary during a render transaction. 177 + 178 + **INV-T4. Identity is drawn from element IDs.** Transition state is associated 179 + with elements by their declared `id`. Callers using transitions on an element 180 + MUST assign it a stable, unique `id` across frames. Reusing an `id` for a 181 + different logical element in a later frame is a caller error; behavior is 182 + unspecified. 183 + 184 + **INV-T5. Animating signal is accurate per transaction.** The `animating` 185 + flag returned by `render()` reflects the state of transitions as of the end 186 + of that transaction. If it is `true`, at least one transition has non-zero 187 + remaining progress and calling `render()` again with positive `deltaTime` 188 + will advance it. 189 + 190 + **INV-T6. Cancellation is structural.** There is no imperative `cancel()` 191 + API. Transitions are cancelled by re-describing the previous target in a 192 + later frame; the transition infrastructure re-anchors the interpolation from 193 + the current visible value to the new target. 194 + 195 + --- 196 + 197 + ## 6. Rendering Contract Additions 198 + 199 + _This section is normative._ 200 + 201 + ### 6.1 `render()` signature 202 + 203 + The `render()` method accepts an optional `deltaTime` field in its options 204 + argument: 205 + 206 + ``` 207 + render(ops: Op[], options?: RenderOptions): RenderResult 208 + 209 + interface RenderOptions { 210 + mode?: "line"; 211 + row?: number; 212 + pointer?: { x, y, down }; 213 + deltaTime?: number; // seconds; overrides Term's internal clock 214 + } 215 + ``` 216 + 217 + Each `render()` call advances transitions by its `deltaTime`: 218 + 219 + - If `deltaTime` is omitted, Term computes it as the monotonic wall-clock 220 + time elapsed since the previous `render()` call. 221 + - If `deltaTime` is provided, it is used verbatim for that frame. 222 + 223 + On every `render()` call, Term captures the current monotonic timestamp as 224 + the reference point for the next implicit delta. The two modes can be 225 + freely mixed, but mixing within a single session is primarily useful for 226 + tests that step time manually and should otherwise be avoided. 227 + 228 + ### 6.2 `RenderResult` addition 229 + 230 + The render result gains one field: 231 + 232 + ``` 233 + interface RenderResult { 234 + output: Uint8Array; 235 + events: PointerEvent[]; 236 + info: RenderInfo; 237 + errors: ClayError[]; 238 + animating: boolean; // NEW 239 + } 240 + ``` 241 + 242 + `animating` is `true` if and only if at least one element has an in-flight 243 + transition at the end of the transaction. 244 + 245 + ### 6.3 The `transition` field on `open()` 246 + 247 + An element may declare a transition by adding a `transition` field to its 248 + open-element directive. The field is optional. Its absence means the element 249 + has no transitions, which is the default. 250 + 251 + The field accepts either shorthand or longhand form (Section 7). 252 + 253 + --- 254 + 255 + ## 7. Declarative Transition Surface 256 + 257 + _This section is descriptive. The shapes may be revised during implementation, 258 + but the architectural commitments above do not change._ 259 + 260 + ### 7.1 Shorthand form 261 + 262 + All listed properties share one duration and one easing: 263 + 264 + ```ts 265 + open("sidebar", { 266 + layout: { width: fixed(20) }, 267 + bg: rgba(30, 30, 30, 255), 268 + transition: { 269 + duration: 0.2, 270 + easing: easeOut(), 271 + properties: ["x", "width", "bg"], 272 + }, 273 + }) 274 + ``` 275 + 276 + ### 7.2 Longhand form 277 + 278 + Each property declares its own duration and easing independently: 279 + 280 + ```ts 281 + open("sidebar", { 282 + transition: [ 283 + { property: "x", duration: 0.3, easing: easeInOut() }, 284 + { property: "width", duration: 0.3, easing: easeInOut() }, 285 + { property: "bg", duration: 0.15, easing: easeOut() }, 286 + ], 287 + }) 288 + ``` 289 + 290 + The shorthand form is expanded to longhand during directive packing. The wire 291 + encoding carries only longhand. 292 + 293 + ### 7.3 Extended form (enter, exit, interaction handling) 294 + 295 + ```ts 296 + open("toast", { 297 + transition: { 298 + properties: [ 299 + { property: "y", duration: 0.25, easing: easeOut() }, 300 + { property: "bg", duration: 0.15, easing: linear() }, 301 + ], 302 + enter: { 303 + independently: false, 304 + from: { y: -2, bg: rgba(0, 0, 0, 0) }, 305 + }, 306 + exit: { 307 + independently: false, 308 + to: { y: -2, bg: rgba(0, 0, 0, 0) }, 309 + paintOrder: "natural", 310 + }, 311 + interactive: false, 312 + }, 313 + }) 314 + ``` 315 + 316 + **`enter.from`** declares deltas relative to the element's target state. The 317 + initial state used by the enter transition is `target + from`. A missing 318 + `from` entry for a given property means the enter transition starts at the 319 + target value for that property (no visible animation on that axis). 320 + 321 + **`exit.to`** declares deltas relative to the element's last-seen state. 322 + The final state used by the exit transition is `initial + to`. 323 + 324 + **`enter.independently` / `exit.independently`** (default `false`) control 325 + whether the element's enter/exit plays when its parent is also entering or 326 + exiting in the same frame. The default couples the element to its parent: 327 + child elements do not play their own enter/exit when the parent is itself 328 + entering or exiting (this prevents cascaded animations when an entire 329 + container mounts or unmounts). Setting `independently: true` opts in to 330 + playing the animation unconditionally. 331 + 332 + **`exit.paintOrder`** controls how an exiting element is drawn relative to 333 + its reflowing siblings during the exit animation. One of: 334 + 335 + - `"natural"` (default) โ€” paints in the element's natural DOM order. 336 + - `"underSiblings"` โ€” paints beneath siblings; reflowing neighbors cover the 337 + exiting element. 338 + - `"overSiblings"` โ€” paints on top of siblings; the exiting element remains 339 + visually prominent until its animation completes. 340 + 341 + **`interactive`** (default `false`) โ€” when `false`, pointer interactions 342 + with the element are disabled while a position transition is in progress. 343 + When `true`, pointer interactions remain enabled throughout position 344 + transitions. 345 + 346 + ### 7.4 Easing helpers 347 + 348 + Exported from the top-level module: 349 + 350 + ```ts 351 + linear() 352 + easeIn() 353 + easeOut() 354 + easeInOut() 355 + cubicBezier(x1: number, y1: number, x2: number, y2: number) 356 + ``` 357 + 358 + Each returns an `Easing` value: a tagged byte with optional parameters. The 359 + easing enum space is deliberately larger than the current surface to allow 360 + future additions (including a potential `custom()` form that bridges to a 361 + JavaScript function) without breaking serialized frames. 362 + 363 + ### 7.5 Property names 364 + 365 + ```ts 366 + type TransitionProperty = 367 + | "x" | "y" | "position" 368 + | "width" | "height" | "size" 369 + | "bg" | "overlay" | "borderColor" 370 + | "cornerRadius" | "borderWidth" 371 + | "all"; 372 + ``` 373 + 374 + Group names (`position`, `size`, `all`) expand to the underlying property 375 + set during packing and are equivalent to listing the constituent properties 376 + explicitly in longhand form. 377 + 378 + --- 379 + 380 + ## 8. Wire Encoding 381 + 382 + _This section is descriptive._ 383 + 384 + The transition block is a new optional tagged section on `OP_OPEN_ELEMENT`. 385 + Its presence is indicated in the element's property bitmask (existing 386 + mechanism for optional fields). When present, its layout is: 387 + 388 + ``` 389 + transition_block { 390 + flags: u8 // bit 0: enter present 391 + // bit 1: exit present 392 + // bit 2: interactive (0 = disabled, 1 = enabled) 393 + entry_count: u8 // number of property_transition entries 394 + entries: property_transition[] // entry_count entries, in stable property order 395 + enter?: transition_side // present iff flags bit 0 396 + exit?: transition_side // present iff flags bit 1 397 + } 398 + 399 + property_transition { 400 + property: u16 // single-bit mask from Clay's property enum 401 + duration: f32 // seconds, non-negative 402 + easing: u8 // easing kind 403 + params: f32[0 or 4] // 4 floats iff easing == cubicBezier 404 + } 405 + 406 + transition_side { 407 + flags: u8 // bit 0: independently 408 + // bits 1-2: paintOrder (exit only: 0 natural, 1 under, 2 over) 409 + mask: u16 // which properties have deltas 410 + values: bytes // packed in stable property order; widths per property 411 + } 412 + ``` 413 + 414 + Value widths are property-specific: `f32` for position and size, `u32` for 415 + colors, `u8[4]` for border widths, `u8[4]` for corner radii (8-bit 416 + resolution per corner is consistent with the existing cornerRadius 417 + encoding). 418 + 419 + The shorthand form is never present on the wire. TS fans shorthand out to 420 + per-property longhand entries before packing. The C side sees only longhand. 421 + 422 + ### 8.1 Validation 423 + 424 + The existing `validate()` utility gains checks: 425 + 426 + - `duration >= 0` for every entry. 427 + - `easing` is one of the defined enum values. 428 + - Property names in entries are valid and appear at most once. 429 + - Property names in `enter.from` / `exit.to` are a subset of the entries 430 + (deltas for a property not being transitioned are ignored or flagged). 431 + 432 + --- 433 + 434 + ## 9. Cancellation Semantics 435 + 436 + _This section is normative._ 437 + 438 + A caller cancels an in-flight transition by emitting a new frame whose 439 + directive for that element describes a different target state. The 440 + transition infrastructure re-anchors the interpolation: 441 + 442 + - The new `initial` value becomes the element's currently-visible value. 443 + - `elapsedTime` resets to zero. 444 + - The new `target` is the value declared in the current frame. 445 + 446 + The transition duration is unchanged. A cancelled-and-reversed transition 447 + takes its full configured duration regardless of how far it had progressed 448 + at the time of cancellation. 449 + 450 + There is no `term.cancelTransition(id)` call. The frame-snapshot model 451 + makes cancellation a structural consequence of re-describing the desired 452 + state rather than an imperative operation. 453 + 454 + --- 455 + 456 + ## 10. Interaction with Line Mode 457 + 458 + _This section is descriptive; the concrete behavior will be finalized 459 + during implementation._ 460 + 461 + Line mode emits cells as newline-separated rows without absolute cursor 462 + positioning. Position transitions (`x`, `y`) have no meaningful effect in 463 + this mode: the rendering output places each row at the current cursor, 464 + not at absolute coordinates. 465 + 466 + Expected behavior in line mode: 467 + 468 + - Color and size transitions proceed normally. 469 + - Position transitions are silently skipped (treated as if the property is 470 + not being transitioned for that frame). 471 + - Enter/exit transitions that declare `from` or `to` deltas on position 472 + properties have those position deltas dropped; other delta properties 473 + still apply. 474 + 475 + The `animating` signal reports accurately regardless of mode; line-mode 476 + color or size transitions still report as animating. 477 + 478 + --- 479 + 480 + ## 11. Testing Strategy 481 + 482 + _This section is descriptive._ 483 + 484 + The `deltaTime` override enables deterministic, snapshot-friendly tests. 485 + A test sequence looks like: 486 + 487 + ```ts 488 + term.render(opsA, { deltaTime: 0 }); 489 + term.render(opsB, { deltaTime: 0 }); // target change, no time elapsed 490 + term.render(opsB, { deltaTime: 0.1 }); // 50% through a 0.2s transition 491 + term.render(opsB, { deltaTime: 0.1 }); // 100%, completed 492 + ``` 493 + 494 + Test coverage should include, at minimum: 495 + 496 + - Shorthand and longhand produce identical output for equivalent configs. 497 + - Enter transitions with `independently: true` and `false`. 498 + - Exit transitions with each `paintOrder` value. 499 + - Cancellation: target change mid-flight re-anchors initial to current. 500 + - Re-appearance during an exit transition. 501 + - Transition config present one frame and absent the next. 502 + - Multiple concurrent transitions on a single element (longhand). 503 + - Multiple concurrent transitions on multiple elements. 504 + - Line mode rendering: color and size transitions apply, position transitions 505 + are silently skipped. 506 + 507 + --- 508 + 509 + ## 12. Implementation Notes 510 + 511 + _This section is descriptive and may change without affecting contract._ 512 + 513 + ### 12.1 Clay submodule version 514 + 515 + clayterm currently pins Clay at commit `76ec363`. The transition API was 516 + introduced upstream in commit `ee192f4`, with follow-up bug fixes. Before 517 + implementing transitions, the Clay submodule must be advanced to a post- 518 + `ee192f4` commit. Non-transition Clay changes introduced between the current 519 + pin and the target pin โ€” notably the `Clay_OnHover` signature change and the 520 + element ID scheme split โ€” require an audit of existing clayterm integration. 521 + 522 + Upgrading Clay is a prerequisite and should be treated as its own commit 523 + ahead of transition work. 524 + 525 + ### 12.2 Handler architecture 526 + 527 + Each `Term` registers a single C-side transition handler with Clay. 528 + Per-element transition metadata (per-property duration, easing, easing 529 + params, enter deltas, exit deltas) is stored in a side table keyed by 530 + Clay element ID, owned by the Term's context. 531 + 532 + The handler: 533 + 534 + 1. Resolves the active Term context. 535 + 2. Looks up metadata for the element by its Clay ID. 536 + 3. For each property in the active bitmask, computes local progress as 537 + `clamp(elapsedTime / property.duration, 0, 1)`, applies the property's 538 + easing, writes the interpolated value into the output struct. 539 + 4. Increments the Term context's `animating_count`. 540 + 5. Returns `true` if any property's local progress is below 1.0. 541 + 542 + At the start of each `render()`, the Term resets its `animating_count` to 543 + zero. At the end, the value is copied into the result struct as the 544 + `animating` flag (true if count > 0). 545 + 546 + The `setInitialState` and `setFinalState` callbacks Clay expects are 547 + implemented as fixed C functions that apply the per-element `from` / `to` 548 + deltas from the side table to the target / initial state Clay passes in. 549 + 550 + ### 12.3 Per-element storage lifetime 551 + 552 + Metadata is repopulated each frame during directive unpacking. Clay's 553 + handler is invoked synchronously inside `Clay_EndLayout`, so per-frame 554 + metadata remains valid when the handler fires. No metadata needs to persist 555 + across frames on our side; Clay's internal hashmap persists the actual 556 + transition state (elapsed time, current value, state machine phase). 557 + 558 + ### 12.4 Multiple Term instances 559 + 560 + `animating_count` and the metadata side table live on the Term's C-side 561 + context, not as module-level state. Multiple Terms created in the same 562 + process remain isolated. 563 + 564 + --- 565 + 566 + ## 13. Open Questions 567 + 568 + These items remain undecided and will be resolved during implementation. 569 + They do not affect the contract. 570 + 571 + ### 13.1 First-frame delta 572 + 573 + On the very first `render()` after `createTerm()`, there is no previous 574 + frame to compute a delta against. Clay's own behavior on its first 575 + `Clay_EndLayout(deltaTime)` call (with a non-zero delta) is the source of 576 + truth: clayterm will pass through whatever delta it has computed and adopt 577 + whatever Clay does. Verification and documentation occur during 578 + integration. 579 + 580 + ### 13.2 Mid-transition target change 581 + 582 + The cancellation semantics in Section 9 require that a target change 583 + mid-flight re-anchors `initial` to the current visible value. Clay's 584 + `TRANSITIONING` state machine is expected to handle this, but it must be 585 + verified. If Clay does not re-anchor, our handler adds the logic by 586 + tracking the last-seen target per element. 587 + 588 + ### 13.3 Element re-appearance mid-exit 589 + 590 + If an element is exiting and reappears in the next frame's directives, 591 + the expected behavior is to cancel the exit and interpolate from the 592 + current visible state to the new target. Implementation-dependent on Clay. 593 + 594 + ### 13.4 Transition removed mid-flight 595 + 596 + If an element has a transition one frame and the `transition` field is 597 + absent in the next frame, Clay's behavior for in-flight transitions 598 + determines the outcome. Two reasonable options: (a) in-flight transitions 599 + complete using their original config; (b) they freeze at their current 600 + value. Deferred to Clay's observed behavior. Documented once verified. 601 + 602 + ### 13.5 Custom easing escape hatch 603 + 604 + The easing enum space is deliberately larger than the initial surface. A 605 + future `custom()` easing that bridges to a JavaScript function is 606 + anticipated but not specified here. Its design must preserve INV-T3 607 + (no callbacks across the boundary during a render transaction) โ€” likely 608 + via a pre-sampled lookup table supplied in the directive buffer. 609 + 610 + --- 611 + 612 + ## 14. Demos 613 + 614 + Two demos accompany the feature: 615 + 616 + 1. **`demo/transitions.ts`** โ€” a clayterm-native demo meaningfully 617 + exercising transitions in a terminal context (e.g., a collapsing 618 + sidebar, a list reorder, or a toast notification). Primary purpose: 619 + surface real-world sharp edges in the API. 620 + 621 + 2. **A reproduction of Clay's upstream `raylib-transitions` demo** โ€” 622 + the example that accompanied the Clay transition-API commit 623 + (`ee192f4`). Primary purpose: provide a reference implementation 624 + that can be visually compared to upstream, validating that the 625 + clayterm integration faithfully exercises the full transition API 626 + surface. 627 + 628 + --- 629 + 630 + ## Appendix A. Relationship to the Renderer Specification 631 + 632 + This specification extends, but does not modify, the renderer specification. 633 + Specifically: 634 + 635 + - **INV-1 (Zero IO).** Transitions introduce reading of a monotonic clock 636 + for `deltaTime` computation. A clock read is not terminal IO and does 637 + not violate this invariant. The renderer still produces bytes only; it 638 + does not read or write terminals. 639 + 640 + - **INV-2 (Single transaction per frame).** Transitions preserve this. 641 + All transition configuration is serialized into the single directive 642 + buffer; no additional boundary crossings occur during rendering. 643 + 644 + - **INV-3 (Frame-snapshot independence).** Transitions preserve this at 645 + the API level. Each directive array still fully describes the desired 646 + state. Element IDs carry more weight (Section 4.1) but callers do not 647 + acquire new cross-frame bookkeeping responsibilities. 648 + 649 + - **INV-4 (ANSI byte output).** Unchanged. 650 + 651 + - **INV-5 (Layout/render/diff ownership).** The renderer additionally 652 + owns transition interpolation. Interpolated values feed into the 653 + existing layout and diff pipeline at the same pipeline stage that 654 + resolved values would. 655 + 656 + The "Deferred/Future Areas" section of the renderer specification should 657 + be updated to remove transitions from its list and to reference this 658 + specification.