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

# Oversized Cursor

> A deliberately oversized macOS-style pointer that enters off-screen, travels to a target, clicks to ignite a visible response, then exits

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="oversized-cursor 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/oversized-cursor.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/oversized-cursor.png");document.body.appendChild(p)});<\/script></body></html>`} />

## Install

<InstallCommand command="npx hyperframes add oversized-cursor" />

That writes one file: `compositions/components/oversized-cursor.html`.

## Paste it into your composition

Open `compositions/components/oversized-cursor.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                                                                                 |
| ---------------- | ---------- | -------------------- | -------------------------------------------------------------------------------------------- |
| `cursor_variant` | `light`    | `light`, `dark`      |                                                                                              |
| `target_x`       | `55`       | 15% to 85%, step 1%  |                                                                                              |
| `target_y`       | `55`       | 15% to 85%, step 1%  |                                                                                              |
| `click_label`    | `Generate` | string               |                                                                                              |
| `exit`           | `none`     | `none`, `fade`, `up` | Optional whole-stage departure. Default none: the ignited target stays until the frame cuts. |

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="oversized-cursor"
  data-composition-src="compositions/components/oversized-cursor.html"
  data-variable-values='{"cursor_variant":"light","target_x":55,"target_y":55,"click_label":"Generate","exit":"none"}'
></div>
```

## Source

<Accordion title={`oversized-cursor.html`}>
  ```html theme={null}
  <!doctype html>
  <!--
    Oversized Cursor (actor primitive), mountable sub-composition

    concept: a deliberately oversized macOS-style pointer that enters the frame
    from off-screen, travels in one continuous glide to a target, taps it (the
    click ignites a visible reaction on the target itself), drifts aside during
    the dwell, then exits off-screen. One mechanic: a pointer-driven click that
    visibly causes something. Family: pointers. Profile: interaction.

    compiled-from: spec ready (oversized-cursor SKILL.md, ~/Downloads/oversized-cursor);
    actor file mechanics compose (size/look, entry law, tip-targeting, click-ignition,
    exit law).

    use-when: kicking off a UI scene, igniting a morph/transition/typing run with a
    causal click, or carrying the eye across a scene that would otherwise read as
    static or stale.

    mount contract: this file is a HyperFrames sub-composition, loaded by a host via
    data-composition-src, never opened standalone. The runtime clones ONLY the
    <template> contents into the host slot (see
    skills/hyperframes-core/references/sub-compositions.md); everything outside
    <template>, including this <head>, is discarded at render. The root is #root:
    elastic, no data-width/data-height declared here, it fills whatever box the host
    clip gives it (position:absolute; inset:0). See demo.html for the mount usage
    (data-composition-src="./oversized-cursor.html" on a sized host clip).

    variables:
      cursor_variant (enum light|dark)  pointer fill/stroke pairing, pick per scene contrast
      target_x, target_y (number, percent of the HOST box)  the tip's landing point
      click_label (string)  label on the target the cursor clicks
      exit (enum none|fade|up, default none)  optional whole-stage departure during
        OUT; the cursor's own off-screen exit always plays (it is the mechanic)

    envelope (of data-composition-duration, default 4.2s):
      IN   0.00s to 1.17s (fixed)     off-screen entry glide (0.85s) then click tap (0.32s)
      HOLD 1.17s to D-0.60s (elastic) click ignition plays, cursor drifts aside, then dwells
      OUT  D-0.60s to D (fixed)       cursor accelerates off-screen; with exit fade|up the
                                      whole stage (target included) also departs

    sound: one click tap SFX (soft UI click) fires at the click moment, the fixed
    offset into IN documented below as SYNC POINT. This file never plays audio: it
    only marks the cue's timing for the mix stage to pick up.
  -->
  <html
    lang="en"
    data-composition-id="oversized-cursor"
    data-composition-duration="4.2"
    data-composition-variables='[
      { "id": "cursor_variant", "type": "enum", "role": "style", "label": "Cursor fill", "default": "light", "options": [ { "value": "light", "label": "Light (white body)" }, { "value": "dark", "label": "Dark (near-black body)" } ] },
      { "id": "target_x", "type": "number", "role": "layout", "label": "Target X", "default": 55, "min": 15, "max": 85, "step": 1, "unit": "%" },
      { "id": "target_y", "type": "number", "role": "layout", "label": "Target Y", "default": 55, "min": 15, "max": 85, "step": 1, "unit": "%" },
      { "id": "click_label", "type": "string", "role": "content", "label": "Click label", "default": "Generate" },
      { "id": "exit", "type": "enum", "role": "timing", "label": "Exit", "description": "Optional whole-stage departure. Default none: the ignited target stays until the frame cuts.", "default": "none", "options": [ { "value": "none", "label": "None" }, { "value": "fade", "label": "Fade" }, { "value": "up", "label": "Up" } ] }
    ]'
  >
    <head>
      <meta charset="UTF-8" />
      <title>Oversized Cursor</title>
      <!-- head is metadata for the source file only; the runtime discards it on mount -->
    </head>
    <body>
      <template>
        <style>
          *,
          *::before,
          *::after {
            box-sizing: border-box;
          }

          /* Root is styled by #root, never a class: the compositor scopes CSS to
             [data-composition-id="oversized-cursor"] as a descendant selector, so a
             rule keyed on the root's own class would never match the root itself.

             Elastic and position-blind: fills whatever box the host slot gives it
             (inset:0), owns its own stacking + container-query context, no
             position:fixed anywhere in this file. */
          #root {
            position: absolute;
            inset: 0;
            overflow: hidden;
            isolation: isolate;
            container-type: size;
            container-name: oc-stage;
            font-family: var(--font-body, Inter, system-ui, sans-serif);
          }

          .oc-stage {
            position: absolute;
            inset: 0;
            /* Transparent by design: this actor overlays whatever scene it is
               dropped into. demo.html supplies its own backdrop around it. */
          }

          /* EDIT ZONE: target look. Safe to retheme via tokens; keep it a simple,
             legible UI surface so the click reaction reads clearly. */
          .oc-target {
            position: absolute;
            left: 50%;
            top: 50%;
            display: flex;
            align-items: center;
            gap: var(--space-1, 0.6cqw);
            padding: var(--space-2, 1.5cqw) var(--space-3, 2.4cqw);
            border-radius: var(--radius, 999px);
            border: 1px solid var(--border, rgba(148, 163, 184, 0.35));
            background: color-mix(
              in srgb,
              var(--surface, #f8fafc) calc((1 - var(--oc-ignite, 0)) * 100%),
              var(--brand, #52525b) calc(var(--oc-ignite, 0) * 100%)
            );
            box-shadow: 0 1.2cqw 3.2cqw rgba(2, 6, 23, 0.24);
            --oc-ignite: 0;
            z-index: 10;
          }

          .oc-target-check {
            width: 1.8cqw;
            height: 1.8cqw;
            min-width: 14px;
            min-height: 14px;
            flex: none;
            fill: none;
            stroke: var(--fg, #ffffff);
            stroke-width: 2.4;
            stroke-linecap: round;
            stroke-linejoin: round;
            opacity: clamp(0, var(--oc-ignite, 0), 1);
            transform: scale(calc(0.5 + var(--oc-ignite, 0) * 0.5));
          }

          .oc-target-label {
            font-size: clamp(12px, 1.7cqw, 30px);
            font-weight: 650;
            line-height: 1;
            white-space: nowrap;
            color: color-mix(
              in srgb,
              var(--fg, #0f172a) calc((1 - var(--oc-ignite, 0)) * 100%),
              var(--fg, #ffffff) calc(var(--oc-ignite, 0) * 100%)
            );
          }

          /* INVARIANT: cursor size floor is 7cqw for full-frame scenes (house
             convention). Never go smaller; an actual-size cursor disappears at
             video scale. */
          .oc-cursor {
            position: absolute;
            left: 0;
            top: 0;
            width: 7cqw;
            height: 7cqw;
            min-width: 34px;
            min-height: 34px;
            pointer-events: none;
            will-change: transform;
            filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.3));
            z-index: 20;
          }

          .oc-cursor-svg {
            display: block;
            width: 100%;
            height: 100%;
            stroke-linejoin: round;
            stroke-linecap: round;
          }

          /* EDIT ZONE: the two house-convention fill pairings. Pick per scene
             contrast, keep it constant per film. */
          .oc-cursor[data-variant="light"] .oc-cursor-svg {
            fill: #ffffff;
            stroke: #141414;
            stroke-width: 1.4;
          }
          .oc-cursor[data-variant="dark"] .oc-cursor-svg {
            fill: #1c1c1c;
            stroke: #ffffff;
            stroke-width: 1.4;
          }

          /* INVARIANT: ripple anchors to the arrow's tip point (21%, 14% of the
             cursor box), the same point transformOrigin pivots the click tap on. */
          .oc-ripple {
            position: absolute;
            left: 21%;
            top: 14%;
            width: 2.6cqw;
            height: 2.6cqw;
            min-width: 12px;
            min-height: 12px;
            border-radius: 999px;
            border: 0.24cqw solid color-mix(in srgb, var(--fg, #ffffff) 45%, transparent);
            transform: translate(-50%, -50%);
            opacity: 0;
          }
        </style>

        <div id="root" data-composition-id="oversized-cursor" data-duration="4.2">
          <div class="oc-stage">
            <div class="oc-target" id="oc-target">
              <svg class="oc-target-check" viewBox="0 0 24 24" aria-hidden="true">
                <path d="M5 13l4 4L19 7" />
              </svg>
              <span class="oc-target-label" id="oc-target-label"></span>
            </div>
            <div class="oc-cursor" id="oc-cursor" data-variant="light" aria-hidden="true">
              <div class="oc-ripple"></div>
              <svg class="oc-cursor-svg" viewBox="0 0 24 24" aria-hidden="true">
                <path d="M5 3 L5 19 L9 15 L12 22 L15 20.5 L11.5 14 L18 14 Z" />
              </svg>
            </div>
          </div>
        </div>

        <script>
          (function () {
            "use strict";

            // NOTE: once mounted, document.documentElement is the HOST page's
            // root element, not this file's own (the mount contract discards
            // this file's head and html elements after the loader reads declared
            // variables/duration once, before cloning). html.getAttribute(...)
            // below therefore resolves to null under mount, and every read
            // falls through to its inline default (55 / 55 / "Generate" /
            // "light"), which match this file's own declared defaults above.
            var html = document.documentElement;
            var root = document.getElementById("root");
            var cursor = document.getElementById("oc-cursor");
            var ripple = cursor.querySelector(".oc-ripple");
            var target = document.getElementById("oc-target");
            var targetLabel = document.getElementById("oc-target-label");

            // Declared defaults, then the render/preview engine's override object.
            var DEFAULTS = {};
            try {
              JSON.parse(html.getAttribute("data-composition-variables") || "[]").forEach(
                function (variable) {
                  DEFAULTS[variable.id] = variable.default;
                },
              );
            } catch (error) {
              /* malformed declaration falls back to hardcoded defaults below */
            }
            // Under mount, the runtime resolves this file's own declared
            // variables merged with the host clip's per-instance
            // data-variable-values through a scoped window.__hyperframes,
            // shadowed in just for this file's script. window.__hfVariables
            // stays in the merge too, as a raw fallback for the (non-mount)
            // case where this file is driven by an older render/preview path.
            var scopedVariables = {};
            try {
              if (window.__hyperframes && typeof window.__hyperframes.getVariables === "function") {
                scopedVariables = window.__hyperframes.getVariables() || {};
              }
            } catch (error) {
              /* no scoped variables API in this context, fall through */
            }
            var vars = Object.assign({}, DEFAULTS, window.__hfVariables || {}, scopedVariables);

            function clampPercent(value, fallback) {
              var n = Number(value);
              if (!isFinite(n)) n = fallback;
              return Math.max(0, Math.min(100, n));
            }

            // Hardcoded per the mount contract: deriving the id from the document
            // returns null once the compositor rewrites the wrapper's attributes,
            // which registers the timeline under "null" and breaks seek binding.
            var compositionId = "oversized-cursor";
            var duration = Math.max(
              0.001,
              parseFloat(
                root.dataset.duration || html.getAttribute("data-composition-duration") || "4.2",
              ),
            );
            var variant = vars.cursor_variant === "dark" ? "dark" : "light";
            var targetX = clampPercent(vars.target_x, 55);
            var targetY = clampPercent(vars.target_y, 55);
            var clickLabel = String(vars.click_label || "Generate");
            var exit = vars.exit === "fade" || vars.exit === "up" ? vars.exit : "none";
            var stage = root.querySelector(".oc-stage");

            cursor.setAttribute("data-variant", variant);
            targetLabel.textContent = clickLabel;
            target.style.left = targetX + "%";
            target.style.top = targetY + "%";

            // Motion runs on transforms only (x/y px), never left/top: layout
            // props snap to integer device pixels under the seek-by-frame
            // capture engine and stutter on slow/eased motion. Stage size is
            // read once, synchronously, at load (deterministic: fixed viewport
            // per render, no resize mid-render) to convert the percent-based
            // target/off-screen positions (percent of the HOST box, not a fixed
            // 1920 stage) into the px x/y GSAP needs.
            var stageW = root.clientWidth || 1920;
            var stageH = root.clientHeight || 1080;
            function xAt(percent) {
              return (percent / 100) * stageW;
            }
            function yAt(percent) {
              return (percent / 100) * stageH;
            }

            // Tip-targeting: the arrow's visual tip sits at (21%, 14%) inside the
            // cursor's own box. Anchor the box by that offset once, up front, so
            // every later x/y tween places the TIP (not the box corner) at the
            // given stage position. Never tween xPercent/yPercent.
            gsap.set(cursor, { xPercent: -21, yPercent: -14 });
            gsap.set(target, { xPercent: -50, yPercent: -50, scale: 1 });

            // RETIME RANGE: IN/OUT are fixed durations (house convention: entry
            // glide 0.4-0.92s + click tap 0.32s). They only shrink, proportionally,
            // if the composition duration is too short to hold IN + OUT at all.
            var IN_BASE = 1.17; // 0.85s glide + 0.1s compress + 0.22s expand
            var OUT_BASE = 0.6;
            var totalBase = IN_BASE + OUT_BASE;
            var IN = IN_BASE;
            var OUT = OUT_BASE;
            if (duration < totalBase) {
              var shrink = duration / totalBase;
              IN = IN_BASE * shrink;
              OUT = OUT_BASE * shrink;
            }
            var HOLD = Math.max(0, duration - (IN + OUT));
            var OUT_AT = IN + HOLD;
            var shrinkFactor = IN / IN_BASE;

            var glideDur = 0.85 * shrinkFactor;
            var clickInDur = 0.1 * shrinkFactor;
            var clickOutDur = 0.22 * shrinkFactor;
            // SYNC POINT: click moment (entry settle), a fixed offset into IN,
            // never inside the elastic HOLD.
            var CLICK_AT = glideDur;

            var offX = xAt(48);
            var offY = yAt(116); // resting pose IS off-screen, below the stage

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

            // ---- IN: off-screen entry, one continuous vector, then the tap ----
            tl.set(cursor, { scale: 1 });
            tl.set(ripple, { opacity: 0, scale: 0.4 });
            tl.set(target, { "--oc-ignite": 0 });
            tl.fromTo(
              cursor,
              { x: offX, y: offY },
              { x: xAt(targetX), y: yAt(targetY), duration: glideDur, ease: "power3.out" },
              0,
            );
            tl.to(
              cursor,
              { scale: 0.84, duration: clickInDur, ease: "power2.in", transformOrigin: "21% 14%" },
              CLICK_AT,
            );
            tl.to(
              cursor,
              { scale: 1, duration: clickOutDur, ease: "power2.out", transformOrigin: "21% 14%" },
              CLICK_AT + clickInDur,
            );

            // ---- the click ignites the target: parallel reaction, same frame ----
            tl.to(target, { scale: 0.94, duration: clickInDur, ease: "power2.in" }, CLICK_AT);
            tl.to(
              target,
              { scale: 1, duration: clickOutDur, ease: "power2.out" },
              CLICK_AT + clickInDur,
            );
            tl.fromTo(
              ripple,
              { opacity: 0.85, scale: 0.4 },
              { opacity: 0, scale: 2.4, duration: 0.42, ease: "power2.out" },
              CLICK_AT + clickInDur,
            );
            tl.to(
              target,
              { "--oc-ignite": 1, duration: 0.34, ease: "back.out(2.2)" },
              CLICK_AT + clickInDur,
            );

            // ---- HOLD (elastic): drift aside once, then dwell, never wobble ----
            var driftDelay = Math.min(0.22, HOLD * 0.3);
            var driftDur = Math.max(0, Math.min(0.7, HOLD - driftDelay));
            if (driftDur > 0.05) {
              var asideX = clampPercent(targetX + (targetX < 50 ? 16 : -16), targetX);
              var asideY = clampPercent(targetY + 10, targetY);
              tl.to(
                cursor,
                {
                  x: xAt(asideX),
                  y: yAt(asideY),
                  duration: driftDur,
                  ease: "power2.out",
                  overwrite: "auto",
                },
                IN + driftDelay,
              );
            }

            // ---- OUT: leave the frame, physically, never a fade-in-place ----
            tl.to(
              cursor,
              { y: yAt(120), duration: OUT, ease: "power2.in", overwrite: "auto" },
              OUT_AT,
            );

            // Optional whole-stage departure on top of the cursor's own exit;
            // exit none leaves the ignited target until the frame cuts.
            if (exit === "fade") {
              tl.to(stage, { opacity: 0, duration: OUT, ease: "power2.in" }, OUT_AT);
            } else if (exit === "up") {
              tl.to(stage, { opacity: 0, y: "-4cqh", duration: OUT, ease: "power2.in" }, OUT_AT);
            }

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

Tagged `motion-primitive` `actor` `cursor` `pointer` `interaction`.

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

- [Toggle Flip](/catalog/components/toggle-flip.md)
- [Simulated Cursor](/catalog/components/simulated-cursor.md)
- [Multiplayer Cursors](/catalog/components/multiplayer-cursors.md)
