> ## Documentation Index
> Fetch the complete documentation index at: https://hyperframes.heygen.com/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.

export const InstallCommand = ({command}) => {
  const [copied, setCopied] = React.useState(false);
  const copy = async () => {
    try {
      if (navigator.clipboard && window.isSecureContext) {
        await navigator.clipboard.writeText(command);
      } else {
        const previous = document.activeElement;
        const scratch = document.createElement("textarea");
        scratch.value = command;
        scratch.setAttribute("readonly", "");
        scratch.style.position = "fixed";
        scratch.style.opacity = "0";
        document.body.appendChild(scratch);
        scratch.select();
        document.execCommand("copy");
        document.body.removeChild(scratch);
        previous?.focus?.();
      }
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    } catch {}
  };
  return <div className="hf-install-command not-prose my-4 flex items-stretch overflow-hidden rounded-xl border border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900">
      <code className="flex-1 overflow-x-auto whitespace-nowrap border-r border-zinc-200 px-4 py-3 font-mono text-sm text-zinc-800 dark:border-zinc-800 dark:text-zinc-100">
        {command}
      </code>
      <button type="button" onClick={copy} data-copied={copied ? "true" : "false"} aria-label={`Copy ${command} to the clipboard`} className="hf-install-copy">
        <svg className="hf-install-copy-clipboard" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M14.25 5.25H7.25C6.14543 5.25 5.25 6.14543 5.25 7.25V14.25C5.25 15.3546 6.14543 16.25 7.25 16.25H14.25C15.3546 16.25 16.25 15.3546 16.25 14.25V7.25C16.25 6.14543 15.3546 5.25 14.25 5.25Z" />
          <path d="M2.80103 11.998L1.77203 5.07397C1.61003 3.98097 2.36403 2.96397 3.45603 2.80197L10.38 1.77297C11.313 1.63397 12.19 2.16297 12.528 3.00097" />
        </svg>
        <svg className="hf-install-copy-check" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M2.75 9.5L6.5 13.25L15.25 4.5" />
        </svg>
      </button>
      <span className="hf-install-copy-status" role="status" aria-live="polite">
        {copied ? "Copied" : ""}
      </span>
    </div>;
};

<iframe className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800" title="typed-prompt preview" loading="lazy" srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/components/typed-prompt.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/typed-prompt.png");document.body.appendChild(p)});<\/script></body></html>`} />

## Install

<InstallCommand command="npx hyperframes add typed-prompt" />

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>
```

## Source

<Accordion title={`typed-prompt.html`}>
  ```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>
  ```
</Accordion>

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

## Related topics

* [Browse the complete Catalog](/catalog)
* [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
* [Build a richer composition](/go-further)


## Related topics

- [Code Terminal Run](/catalog/components/code-terminal-run.md)
- [Code animations](/prompting/code-blocks.md)
- [Storyboards](/prompting/storyboards.md)
