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

# Typed Prompt

> A prompt line types itself in chunked human cadence behind a deterministic blinking caret, with optional backspace-and-retype correction of the final word.

# Typed Prompt

A prompt line types itself in chunked human cadence behind a deterministic blinking caret, with optional backspace-and-retype correction of the final word.

**Typed Prompt**

```html typed-prompt.html theme={null}
<!doctype html>
<!--
  typed-prompt: HyperFrames video primitive (product demo / typing)

  A prompt line types itself in chunked human cadence behind a deterministic
  blinking caret. Every visible frame is read from one lookup table of
  text-at-time rows built synchronously before the timeline registers, so
  forward seeks, reverse seeks, and repeated seeks to the same time always
  produce the same string. Nothing is incremental.

  Caret law: a sine phase runs an INTEGER number of cycles across exactly the
  typing window, then a single set() pins the caret solid for the hold.

  Correction mode: when the `correction` variable is non-empty, that string is
  typed IN PLACE of the final word first (the "typo"), a beat passes, it is
  backspaced one character per row, and the true final word of `text` is
  retyped. The resting text always equals `text`.

  Variables (declared in data-composition-variables below):
    - text (string): the prompt that gets typed.
    - prompt_glyph (string, default ">"): leading glyph; empty string hides it.
    - cadence ("uniform" | "human", default "human"): keystroke rhythm.
      uniform = one character per fixed step; human = seeded 1-3 char chunks
      with varied gaps (fixed LCG seed, fully deterministic).
    - caret ("blink" | "solid", default "blink"): blink runs integer sine
      cycles during typing then goes solid; solid never blinks.
    - correction (string, default ""): the mistyped word. Empty disables.
    - cues (string, default ""): comma-separated seconds relative to mount
      start; cue N anchors the start of word N. Empty = authored rhythm.
    - accent (green | blue | violet, default "green"): glyph + caret color.
    - exit ("none" | "fade" | "up", default "none"): frame roots own
      transitions; none holds the end frame.

  Envelope, authored 4s, fixed IN/OUT with elastic HOLD only (never
  gsap.timeScale()): panel settles over 0.5s, typing runs from 0.35s at its
  natural cadence, compressed proportionally only if it would overflow
  duration - OUT. HOLD absorbs all extra duration (solid caret, barely-there
  drift ending in stillness). OUT exists only when exit != none.

  Mount contract: mountable sub-composition; the runtime clones only this
  <template>. #root is elastic (no data-width/height, container-type: size),
  styled via #root only, and registers one paused timeline under the literal
  "typed-prompt" key.
-->
<html
  lang="en"
  data-composition-id="typed-prompt"
  data-composition-duration="4"
  data-composition-variables='[
    { "id": "text", "type": "string", "role": "content", "label": "Text", "description": "The prompt that gets typed.", "default": "Generate a product launch video" },
    { "id": "prompt_glyph", "type": "string", "role": "style", "label": "Prompt glyph", "description": "Leading glyph before the typed line. Empty string hides it.", "default": ">" },
    { "id": "cadence", "type": "enum", "role": "timing", "label": "Cadence", "description": "Keystroke rhythm: uniform fixed steps or seeded human chunks.", "default": "human", "options": [{ "value": "uniform", "label": "Uniform" }, { "value": "human", "label": "Human" }] },
    { "id": "caret", "type": "enum", "role": "style", "label": "Caret", "description": "Blink runs integer sine cycles during typing then goes solid.", "default": "blink", "options": [{ "value": "blink", "label": "Blink" }, { "value": "solid", "label": "Solid" }] },
    { "id": "correction", "type": "string", "role": "content", "label": "Correction", "description": "Mistyped word typed in place of the final word, then backspaced and corrected. Empty disables.", "default": "" },
    { "id": "cues", "type": "string", "role": "timing", "label": "Cues", "description": "Comma-separated seconds relative to mount start; cue N anchors the start of word N. Empty uses the authored rhythm.", "default": "" },
    { "id": "accent", "type": "enum", "role": "style", "label": "Accent", "description": "Color of the prompt glyph and caret.", "default": "green", "options": [{ "value": "green", "label": "Green" }, { "value": "blue", "label": "Blue" }, { "value": "violet", "label": "Violet" }] },
    { "id": "exit", "type": "enum", "role": "timing", "label": "Exit", "description": "Outgoing transition. Frame roots own transitions; none holds the end frame.", "default": "none", "options": [{ "value": "none", "label": "None" }, { "value": "fade", "label": "Fade" }, { "value": "up", "label": "Up" }] }
  ]'
>
  <head>
    <meta charset="UTF-8" />
    <title>Typed Prompt</title>
  </head>
  <body>
    <template>
      <div id="root" data-composition-id="typed-prompt" data-duration="4" data-fps="30">
        <style>
          *,
          *::before,
          *::after {
            box-sizing: border-box;
          }

          #root {
            position: absolute;
            inset: 0;
            overflow: hidden;
            container-type: size;
            isolation: isolate;
            color: var(--fg, #f8fafc);
            font-family: var(--font-mono, ui-monospace, "SF Mono", Menlo, Consolas, monospace);
            pointer-events: none;
          }

          .tp-clip {
            position: absolute;
            inset: 0;
            display: grid;
            place-items: center;
            overflow: hidden;
            background: var(--bg, transparent);
          }

          .tp-stage {
            width: 82cqw;
            max-width: 82cqw;
            will-change: transform, opacity;
          }

          .tp-shell {
            display: flex;
            align-items: flex-start;
            gap: var(--space-1, 1.6cqw);
            min-height: 18cqh;
            padding: var(--space-2, 3.4cqh) var(--space-2, 3cqw);
            border: 0.16cqmin solid color-mix(in srgb, var(--border, #475569) 72%, transparent);
            border-radius: var(--radius, 2.4cqmin);
            background: color-mix(in srgb, var(--surface, #0b1016) 94%, var(--tp-accent));
            box-shadow: 0 3cqh 9cqh color-mix(in srgb, var(--bg, #000000) 38%, transparent);
          }

          .tp-glyph {
            flex: 0 0 auto;
            color: var(--tp-accent);
            font-size: var(--tp-font-size);
            font-weight: 600;
            line-height: 1.45;
          }

          .tp-line {
            flex: 1 1 auto;
            min-width: 0;
            color: var(--fg, #f8fafc);
            font-size: var(--tp-font-size);
            font-weight: 500;
            line-height: 1.45;
            overflow-wrap: anywhere;
            white-space: pre-wrap;
          }

          .tp-caret {
            display: inline-block;
            width: 0.55ch;
            height: 1.05em;
            margin-left: 0.08ch;
            background: var(--tp-accent);
            vertical-align: text-bottom;
            will-change: opacity;
          }
        </style>

        <div
          id="typed-prompt-clip"
          class="tp-clip clip"
          data-start="0"
          data-duration="4"
          data-track-index="0"
        >
          <div class="tp-stage" role="img">
            <div class="tp-shell">
              <span class="tp-glyph" aria-hidden="true"></span>
              <span class="tp-line" aria-hidden="true"
                ><span class="tp-text"></span><span class="tp-caret"></span
              ></span>
            </div>
          </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");
            var stage = root.querySelector(".tp-stage");
            var glyphElement = root.querySelector(".tp-glyph");
            var textElement = root.querySelector(".tp-text");
            var caretElement = root.querySelector(".tp-caret");
            var vars =
              window.__hyperframes && window.__hyperframes.getVariables
                ? window.__hyperframes.getVariables()
                : {};

            var text = vars.text == null ? "Generate a product launch video" : String(vars.text);
            var promptGlyph = vars.prompt_glyph == null ? ">" : String(vars.prompt_glyph);
            var cadence = vars.cadence === "uniform" ? "uniform" : "human";
            var caretMode = vars.caret === "solid" ? "solid" : "blink";
            var correction = vars.correction == null ? "" : String(vars.correction).trim();
            var exitMode = vars.exit === "fade" || vars.exit === "up" ? vars.exit : "none";

            // Each enum choice routes to a DIFFERENT contract token so the
            // variable stays meaningful under a theme.
            var accentColors = {
              green: "var(--brand, #71f5a7)",
              blue: "var(--accent, #61a8ff)",
              violet: "var(--accent-2, #c5a3ff)",
            };
            var accent = Object.prototype.hasOwnProperty.call(accentColors, vars.accent)
              ? vars.accent
              : "green";
            root.style.setProperty("--tp-accent", accentColors[accent]);
            glyphElement.textContent = promptGlyph;
            if (promptGlyph === "") glyphElement.style.display = "none";
            stage.setAttribute("aria-label", text);

            var cueList = String(vars.cues == null ? "" : vars.cues)
              .split(",")
              .map(function (token) {
                return parseFloat(token);
              })
              .filter(function (value) {
                return isFinite(value) && value >= 0;
              })
              .sort(function (a, b) {
                return a - b;
              });

            var ENTER = 0.5;
            var TYPE_START = 0.35;
            var OUT_BASE = 0.45;
            var STILLNESS = 0.3;
            var duration = Math.max(0.001, parseFloat(root.dataset.duration || "4"));
            var OUT = exitMode === "none" ? 0 : Math.min(OUT_BASE, duration * 0.2);

            // Fixed-seed LCG: the only randomness source, consumed in one
            // deterministic order during this synchronous build.
            var lcgState = 0x7f4a7c15;
            function rnd() {
              lcgState = (Math.imul(1664525, lcgState) + 1013904223) >>> 0;
              return lcgState / 4294967296;
            }
            function chunksOf(word) {
              if (cadence === "uniform") return Array.from(word);
              var out = [];
              var i = 0;
              while (i < word.length) {
                var n = 1 + Math.floor(rnd() * 3);
                out.push(word.slice(i, i + n));
                i += n;
              }
              return out;
            }
            function chunkGap(chunk) {
              if (cadence === "uniform") return chunk.length * 0.055;
              return chunk.length * (0.05 + rnd() * 0.04);
            }

            // The complete text-at-time table. Built exactly once; the
            // timeline only ever reads it, so seeks in any direction resolve
            // the same string for the same time.
            var rows = [{ t: 0, x: "" }];
            var words = text.split(/\s+/).filter(function (word) {
              return word.length > 0;
            });
            var hasCorrection = correction !== "" && words.length > 0;
            (function buildRows() {
              var t = TYPE_START;
              var acc = "";
              words.forEach(function (word, wordIndex) {
                var isLast = wordIndex === words.length - 1;
                var typedWord = hasCorrection && isLast ? correction : word;
                if (wordIndex > 0) acc += " ";
                if (cueList[wordIndex] != null) {
                  t = Math.max(t, cueList[wordIndex]);
                } else if (wordIndex > 0) {
                  t += cadence === "uniform" ? 0.11 : 0.1 + rnd() * 0.1;
                }
                chunksOf(typedWord).forEach(function (chunk) {
                  t += chunkGap(chunk);
                  acc += chunk;
                  rows.push({ t: t, x: acc });
                });
                if (hasCorrection && isLast) {
                  t += 0.4; // the beat of realizing the typo
                  for (var back = 1; back <= correction.length; back += 1) {
                    t += 0.055;
                    rows.push({ t: t, x: acc.slice(0, acc.length - back) });
                  }
                  acc = acc.slice(0, acc.length - correction.length);
                  t += 0.22;
                  chunksOf(word).forEach(function (chunk) {
                    t += chunkGap(chunk);
                    acc += chunk;
                    rows.push({ t: t, x: acc });
                  });
                }
              });
            })();

            // Compress typing proportionally only if it would overflow the
            // window before OUT; extra duration becomes HOLD, never timeScale.
            var naturalEnd = rows[rows.length - 1].t;
            var maxEnd = Math.max(TYPE_START + 0.2, duration - OUT - STILLNESS);
            if (naturalEnd > maxEnd) {
              var factor = (maxEnd - TYPE_START) / (naturalEnd - TYPE_START);
              rows.forEach(function (row) {
                if (row.t > TYPE_START) row.t = TYPE_START + (row.t - TYPE_START) * factor;
              });
            }
            var typeEnd = Math.max(rows[rows.length - 1].t, TYPE_START + 0.01);

            // 1ms tolerance: GSAP snaps a tween's end value below the row
            // table's accumulated float times, and 1ms is far under one frame.
            function textAt(time) {
              for (var i = rows.length - 1; i >= 0; i -= 1) {
                if (time >= rows[i].t - 0.001) return rows[i].x;
              }
              return "";
            }

            // +2: leave room for the trailing caret so it never wraps alone.
            var maxLength =
              rows.reduce(function (best, row) {
                return Math.max(best, row.x.length);
              }, 1) + 2;
            var fittedCqw = Math.max(2.2, Math.min(4.2, 118 / maxLength));
            root.style.setProperty("--tp-font-size", "min(" + fittedCqw.toFixed(3) + "cqw, 9cqh)");

            gsap.set(stage, { opacity: 0, y: "2.6cqh" });
            textElement.textContent = "";

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

            // IN: one panel settle, then the table drives every visible frame.
            tl.to(stage, { opacity: 1, duration: ENTER, ease: "power2.out" }, 0);
            tl.to(stage, { y: "0cqh", duration: ENTER, ease: "power3.out" }, 0);

            var typeDriver = { t: 0 };
            tl.to(
              typeDriver,
              {
                t: typeEnd,
                duration: typeEnd,
                ease: "none",
                onUpdate: function () {
                  textElement.textContent = textAt(typeDriver.t);
                },
              },
              0,
            );

            // Caret: integer sine cycles across exactly the typing window,
            // then a single pin to solid. Solid mode never blinks.
            if (caretMode === "blink") {
              var cycles = Math.max(1, Math.round(typeEnd / 0.9));
              var blink = { p: 0 };
              tl.to(
                blink,
                {
                  p: Math.PI * 2 * cycles,
                  duration: typeEnd,
                  ease: "none",
                  onUpdate: function () {
                    // >= 0 keeps phase 0 identical to the caret's unrendered
                    // initial state (opacity 1) for forward vs backward seeks.
                    caretElement.style.opacity = Math.sin(blink.p) >= 0 ? "1" : "0";
                  },
                },
                0,
              );
              tl.set(caretElement, { opacity: 1 }, typeEnd);
            }

            // HOLD: barely-there drift ending at exactly zero, then authored
            // stillness before any departure.
            var OUT_START = duration - OUT;
            var holdSpan = OUT_START - typeEnd;
            if (holdSpan > 0.9) {
              var driftHalf = (holdSpan - STILLNESS) / 2;
              tl.to(stage, { y: "-0.45cqh", duration: driftHalf, ease: "sine.inOut" }, typeEnd);
              tl.to(
                stage,
                { y: "0cqh", duration: driftHalf, ease: "sine.inOut" },
                typeEnd + driftHalf,
              );
            }

            // OUT: only when the exit variable asks for one.
            if (exitMode !== "none") {
              tl.to(stage, { opacity: 0, duration: OUT, ease: "power2.in" }, OUT_START);
              if (exitMode === "up") {
                tl.to(stage, { y: "-4cqh", duration: OUT, ease: "power2.in" }, OUT_START);
              }
            }

            tl.seek(0);
            window.__timelines = window.__timelines || {};
            window.__timelines["typed-prompt"] = tl;
          })();
        </script>
      </div>
    </template>
  </body>
</html>
```

Copy npx hyperframes add typed-prompt to the clipboard

That writes one file: `compositions/components/typed-prompt.html`.

## Paste it into your composition

Open `compositions/components/typed-prompt.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 |
| - | - | - | - |
| `text` | `Generate a product launch video` | string | The prompt that gets typed. |
| `prompt_glyph` | `>` | string | Leading glyph before the typed line. Empty string hides it. |
| `cadence` | `human` | `uniform`, `human` | Keystroke rhythm: uniform fixed steps or seeded human chunks. |
| `caret` | `blink` | `blink`, `solid` | Blink runs integer sine cycles during typing then goes solid. |
| `correction` | \`\` | string | Mistyped word typed in place of the final word, then backspaced and corrected. Empty disables. |
| `cues` | \`\` | string | Comma-separated seconds relative to mount start; cue N anchors the start of word N. Empty uses the authored rhythm. |
| `accent` | `green` | `green`, `blue`, `violet` | Color of the prompt glyph and caret. |
| `exit` | `none` | `none`, `fade`, `up` | Outgoing transition. Frame roots own transitions; none holds the end frame. |

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 theme={null}
<div
  data-composition-id="typed-prompt"
  data-composition-src="compositions/components/typed-prompt.html"
  data-variable-values='{"text":"Generate a product launch video","prompt_glyph":">","cadence":"human","caret":"blink","correction":"","cues":"","accent":"green","exit":"none"}'
></div>
```

Tagged `motion-primitive` `product-demo` `typing` `prompt` `deterministic`.

## 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)


## Related topics

- [Notes Typing](/catalog/components/notes-typing.md)
- [A full film, prompt by prompt](/prompting/capstone.md)
- [Text, captions and code on screen](/prompting/captions-catalog.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.