> ## Documentation Index
> Fetch the complete documentation index at: /llms.txt
> Use this file to discover all available pages before exploring further.

# Beat Timeline

> A paused master-timeline orchestration spine that pins titled beat rows to evenly spaced named labels. Each row enters on its label, holds with the sequence, then exits with the group.

# Beat Timeline

A paused master-timeline orchestration spine that pins titled beat rows to evenly spaced named labels. Each row enters on its label, holds with the sequence, then exits with the group.

**Beat Timeline**

```html beat-timeline.html
<!doctype html>
<!--
  beat-timeline -- HyperFrames video primitive (transitions / sequencing / bridge)

  Concept: a visible orchestration spine made from a paused master GSAP
  timeline. Each beat row is pinned to an evenly spaced named label, then
  enters exactly on that mark. One mechanic, one job: bridge a sequence by
  making its deterministic beat grid readable and copy-and-own-able.

  Compiled-from evidence: launches-motion-vocabulary.md, "Paused, seeked
  timeline orchestration" census entry (15/18 projects, 134/159 files).

  Use when: a scene needs several titled beats to land in a clear order, or
  when a copied composition needs a legible master-timeline sequencing spine.

  Variables (declared in data-composition-variables below):
    - beatCount (number, default 4): integer row count, clamped to 2..6.
    - interval (number, default 0.6s): base spacing between named beat labels,
      clamped to 0.2..2s before any short-duration compression.
    - labels (string): comma-separated row titles. Missing entries fall back
      to "Beat N" and extra entries are ignored.

  Envelope (holdable orchestration adaptation, never gsap.timeScale()):
    ENTRANCE_SPAN = (beatCount - 1) * interval + ROW_IN_DURATION.
    HOLD = max(0, D - (ENTRANCE_SPAN + OUT_BASE)); every landed row remains
           fully visible and deliberately still during this elastic phase.
    OUT_BASE = 0.40s; all rows fade together with power2.in at D - OUT_BASE.
    If D is shorter than ENTRANCE_SPAN + OUT_BASE, interval and each row's
    entrance duration scale down together so the final row still lands before
    OUT. Only a host duration at or below OUT_BASE also compresses OUT, which
    keeps the full lifecycle inside an otherwise impossible time box.

  Sync points: beat labels are fixed offsets inside IN. Every label owns one
  declared SFX cue at the same offset, never inside elastic HOLD. Beats use
  "beat-tick" and the last beat uses "beat-tick-final". The primitive only
  dispatches bubbling hf:sfx events; scene-level audio mixing owns playback.

  Mount contract: this file is a mountable sub-composition. The runtime clones
  only <template> contents, so all live styles, markup, and scripts are inside
  the template. #root fills the host-owned box with no data-width/data-height,
  and all internal geometry is based on its own cqw/cqh container. Variables
  come from window.__hyperframes.getVariables(), because document.documentElement
  belongs to the host after mounting.
-->
<html
  lang="en"
  data-composition-variables='[
    { "id": "beatCount", "type": "number", "role": "content", "label": "Beat count", "description": "Number of beat rows in the sequence.", "default": 4, "min": 2, "max": 6, "step": 1 },
    { "id": "interval", "type": "number", "role": "layout", "label": "Interval", "description": "Base time between consecutive beat markers.", "default": 0.6, "min": 0.2, "max": 2, "step": 0.05, "unit": "s" },
    { "id": "labels", "type": "string", "role": "content", "label": "Labels", "description": "Comma-separated beat titles, one per beat. Extra entries are ignored and missing entries fall back to Beat N.", "default": "One,Two,Three,Four" }
  ]'
>
  <head>
    <meta charset="UTF-8" />
    <title>Beat Timeline</title>
    <!-- Metadata only. The loader reads variable declarations from <html>,
         then discards everything outside <template> during mounting. -->
  </head>
  <body>
    <template>
      <div
        id="root"
        data-composition-id="beat-timeline"
        data-start="0"
        data-duration="5.5"
        data-fps="30"
      >
        <style>
          *,
          *::before,
          *::after {
            box-sizing: border-box;
          }

          /* Root is styled by ID for mounted CSS scoping. It owns no stage
             dimensions, only the host box and the container-unit basis. */
          #root {
            --ease-standard: cubic-bezier(0.215, 0.61, 0.355, 1);
            --ease-exit: cubic-bezier(0.55, 0.055, 0.675, 0.19);
            position: absolute;
            inset: 0;
            container-type: size;
            isolation: isolate;
            overflow: hidden;
            background: var(--bg, Canvas);
            color: var(--fg, CanvasText);
            font-family: var(--font-body, system-ui, sans-serif);
          }

          .bt-clip {
            width: 100%;
            height: 100%;
            display: grid;
            place-items: center;
          }

          .bt-stack {
            width: 84cqw;
            display: flex;
            flex-direction: column;
            gap: 1.4cqh;
          }

          /* EDIT ZONE: row geometry only. Motion timing is owned by the script's
             RETIME RANGE below, and theme paint remains token-driven here. */
          .bt-row {
            min-height: 10cqh;
            display: grid;
            grid-template-columns: 6cqw 1fr;
            align-items: center;
            column-gap: 2.6cqw;
            padding: 1.4cqh 2.4cqw;
            border: 0.12cqw solid var(--border, currentColor);
            border-radius: var(--radius, 2cqh);
            background: var(--surface, Canvas);
            box-shadow: 0 1.2cqh 4cqh color-mix(in srgb, var(--bg, Canvas) 52%, transparent);
          }

          .bt-index {
            width: 6cqw;
            height: 6cqw;
            display: grid;
            place-items: center;
            border-radius: 50%;
            background: var(--brand, currentColor);
            color: var(--bg, Canvas);
            font-family: var(--font-display, system-ui, sans-serif);
            font-size: 1.8cqw;
            font-weight: 750;
            line-height: 1;
          }

          .bt-copy {
            min-width: 0;
          }

          .bt-title {
            overflow: hidden;
            color: var(--fg, CanvasText);
            font-family: var(--font-display, system-ui, sans-serif);
            font-size: min(4.4cqw, 5.2cqh);
            font-weight: 700;
            line-height: 1.04;
            letter-spacing: -0.08cqw;
            text-overflow: ellipsis;
            white-space: nowrap;
          }

          .bt-subtitle {
            margin-top: 0.7cqh;
            color: var(--muted, currentColor);
            font-size: min(1.8cqw, 2.2cqh);
            font-weight: 550;
            line-height: 1.15;
            letter-spacing: 0.04cqw;
          }
        </style>

        <div
          id="beat-timeline-clip"
          class="bt-clip clip"
          data-start="0"
          data-duration="5.5"
          data-track-index="0"
        >
          <div class="bt-stack" aria-label="Beat timeline"></div>
        </div>

        <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
        <script>
          (function () {
            "use strict";

            var root = document.getElementById("root");
            // Literal by contract: mounting strips data-composition-id from the
            // flattened inner root, so reading the attribute post-mount would
            // register under null and fail the timeline lookup.
            var compositionId = "beat-timeline";
            var stack = root.querySelector(".bt-stack");

            // EDIT ZONE: declarations live on <html>; this runtime call is the
            // single owner of declared defaults plus per-instance overrides.
            var vars =
              window.__hyperframes && window.__hyperframes.getVariables
                ? window.__hyperframes.getVariables()
                : {};

            var parsedBeatCount = Number(vars.beatCount);
            // INVARIANT: exactly 2..6 integer rows reach the DOM and timeline.
            var beatCount = Math.max(
              2,
              Math.min(6, Math.round(Number.isFinite(parsedBeatCount) ? parsedBeatCount : 4)),
            );
            var parsedInterval = Number(vars.interval);
            // INVARIANT: invalid timing falls back once, valid timing clamps to
            // the declared range before duration elasticity is calculated.
            var interval = Math.max(
              0.2,
              Math.min(2, Number.isFinite(parsedInterval) ? parsedInterval : 0.6),
            );
            // INVARIANT: labels is always a string; blank or missing entries
            // fall back per row, while authored extra entries stay unused.
            var labelsValue = vars.labels == null ? "One,Two,Three,Four" : String(vars.labels);
            var suppliedLabels = labelsValue.split(",").map(function (label) {
              return label.trim();
            });

            var parsedDuration = parseFloat(root.dataset.duration || "5.5");
            // INVARIANT: the lifecycle remains finite even under a malformed
            // host duration. The authored and demo durations are both 5.5s.
            var duration = Math.max(0.001, Number.isFinite(parsedDuration) ? parsedDuration : 5.5);

            // RETIME RANGE: these values own the whole pacing decision. The
            // interval and row entrance compress together, never via timeScale.
            var ROW_IN_BASE = 0.4;
            var OUT_BASE = 0.4;
            var baseEntranceSpan = (beatCount - 1) * interval + ROW_IN_BASE;
            var OUT = duration <= OUT_BASE ? duration / 2 : OUT_BASE;
            var entranceBudget = duration - OUT;
            var scale = baseEntranceSpan > entranceBudget ? entranceBudget / baseEntranceSpan : 1;
            var timedInterval = interval * scale;
            var ROW_IN = ROW_IN_BASE * scale;
            var ENTRANCE_SPAN = (beatCount - 1) * timedInterval + ROW_IN;
            var HOLD = Math.max(0, duration - (ENTRANCE_SPAN + OUT));
            var OUT_START = ENTRANCE_SPAN + HOLD;

            function fireSfx(id, t) {
              root.dispatchEvent(
                new CustomEvent("hf:sfx", { detail: { id: id, t: t }, bubbles: true }),
              );
            }

            var rows = [];
            for (var i = 0; i < beatCount; i += 1) {
              var title = suppliedLabels[i] || "Beat " + (i + 1);
              var row = document.createElement("div");
              row.className = "bt-row";
              row.innerHTML =
                '<div class="bt-index" aria-hidden="true"></div>' +
                '<div class="bt-copy"><div class="bt-title"></div><div class="bt-subtitle"></div></div>';
              row.querySelector(".bt-index").textContent = String(i + 1).padStart(2, "0");
              row.querySelector(".bt-title").textContent = title;
              row.querySelector(".bt-subtitle").textContent =
                "Cue " +
                String(i + 1).padStart(2, "0") +
                "  ·  " +
                (i * timedInterval).toFixed(2) +
                "s";
              stack.appendChild(row);
              rows.push(row);
            }

            // Explicit entrance and exit endpoints make a direct seek to t=0
            // correct without ever playing the timeline.
            gsap.set(rows, { opacity: 0, y: "2cqh", scale: 0.98 });

            var tl = gsap.timeline({ paused: true });

            // ORCHESTRATION SPINE: every row tween and its declared cue are
            // pinned to the named label, never to a duplicated numeric offset.
            for (var beatIndex = 0; beatIndex < beatCount; beatIndex += 1) {
              var labelName = "beat" + beatIndex;
              var cueTime = beatIndex * timedInterval;
              var cueId = beatIndex === beatCount - 1 ? "beat-tick-final" : "beat-tick";
              tl.addLabel(labelName, cueTime);
              tl.to(
                rows[beatIndex],
                { opacity: 1, y: "0cqh", scale: 1, duration: ROW_IN, ease: "power2.out" },
                labelName,
              );
              // SFX CUE (IN, beat N): one scene-mixed cue at this beat label.
              tl.call(fireSfx, [cueId, cueTime], labelName);
            }

            // HOLD is deliberately still. OUT accelerates every landed row away
            // together, asymmetrically paired with the power2.out entrances.
            tl.to(rows, { opacity: 0, duration: OUT, ease: "power2.in" }, OUT_START);

            tl.seek(0);

            window.__timelines = window.__timelines || {};
            window.__timelines[compositionId] = tl;
          })();
        </script>
      </div>
    </template>
  </body>
</html>
```

Copy npx hyperframes add beat-timeline to the clipboard

That writes one file: `compositions/components/beat-timeline.html`.

## Paste it into your composition

Open `compositions/components/beat-timeline.html` and copy what is inside into your own composition.

A component has no size or duration of its own. It takes both from the composition
you paste it into.

## Variables

Every one of these has a default, so the piece works untouched. Set the ones you
want to change on the element:

| Variable | Default | Accepts | What it does |
| - | - | - | - |
| `beatCount` | `4` | 2 to 6, step 1 | Number of beat rows in the sequence. |
| `interval` | `0.6` | 0.2s to 2s, step 0.05s | Base time between consecutive beat markers. |
| `labels` | `One,Two,Three,Four` | string | Comma-separated beat titles, one per beat. Extra entries are ignored and missing entries fall back to Beat N. |

Set them with `data-variable-values` on the element that mounts it. These are the
defaults, so this behaves exactly like the preview above until you change one:

```html wrap
<div
  data-composition-id="beat-timeline"
  data-composition-src="compositions/components/beat-timeline.html"
  data-variable-values='{"beatCount":4,"interval":0.6,"labels":"One,Two,Three,Four"}'
></div>
```

{/* hf:generated-footer */}

Tagged `orchestration` `transitions` `sequencing` `bridge` `timeline`.

## Related topics

* [Browse the complete Catalog](/catalog.md)
* [Add assets and Catalog items in the browser editor](/studio/assets-and-blocks.md)
* [Put blocks together into one video](/concepts/compositions.md)
