Skip to content

motionContextToDocs: y track copies opacity when a node animates both (key lookup has no word boundary) #6756

Description

@FelipeOdds

Describe the bug

In @hyperframes/core (dist/figma/motionContextToDocs.js), arrayAfterKey, scalarAfterKey and the
balancedBlock(transition, ${prop}: {) lookup search for the key with a plain regex or substring,
with no word boundary. When a Figma node animates opacity and y together, the search for y: [
matches inside opacity: [ (and y: { inside opacity: {), so the y track gets the opacity values
and timing. Letters that should rise 120px move 1px. Any key that ends another key is exposed the same
way.

The verify-motion.mjs check in the /figma skill passes the wrong translation: on our test it
scored the broken output higher than the corrected one (16.28 dB against 15.89 dB worst segment),
because the fade dominates the motion energy it compares.

Minimal reproduction

// repro.mjs — run: node repro.mjs <path to an installed @hyperframes/core>
const core = process.argv[2];
const { motionContextToDocs } = await import(core + "/dist/figma/index.js");
const snippet = '<motion.div initial={{ opacity: 0, y: 120, }} animate={{ opacity: [0, 0, 1, 1], y: [120, 120, 0, 0], }} transition={{ opacity: { duration: 4.5, times: [0, 0.0444, 0.1333, 1], ease: ["linear", "easeOut", "linear"], repeat: Infinity }, y: { duration: 4.5, times: [0, 0.0444, 0.2222, 1], ease: ["linear", "easeOut", "linear"], repeat: Infinity }, }} />';
const response = {
  nodes: [{ nodeId: "1:2", nodeName: "Box", nodeType: "FRAME", codeSnippets: { motionDev: snippet } }],
  timelineCohorts: [{ rootNodeId: "1:1", durationMs: 4500, loopMode: "loop", memberNodeIds: ["1:2"] }],
};
console.log(JSON.stringify(motionContextToDocs(response, { selectorFor: () => "#box" }), null, 1));

The snippet has the same shape as what get_motion_context returns for a layer with a fade and a
rise in Figma Motion.

Steps to reproduce

  1. npm pack @hyperframes/[email protected] && tar xzf hyperframes-core-0.8.217.tgz && (cd package && npm install --omit=dev)
  2. node repro.mjs ./package
  3. Look at the y track.

Expected behavior

y track: values [120, 120, 0, 0], times [0, 0.0444, 0.2222, 1].

Actual behavior

y track: values [0, 0, 1, 1], times [0, 0.0444, 0.1333, 1]: the opacity track, copied.

Environment

✓ Version          CLI 0.8.181 (plugin) · @hyperframes/core 0.8.181 and 0.8.217 both affected
✓ Node.js          v24.22.0 (darwin arm64)
✓ CPU              8 cores · Apple M3
✓ FFmpeg           ffmpeg 6.0
✓ Chrome           chrome-headless-shell mac_arm 152.0.7977.30

Additional context

A fix that keeps the current parsing: anchor the key so it cannot match inside a longer identifier,
for example new RegExp(`(?<![\\w$])${escapeRegExp(key)}\\s*:\\s*\\[`) in arrayAfterKey, the same
in scalarAfterKey, and the same guard in the balancedBlock lookup for ${prop}: {. Our workaround
until then is to reorder the snippet so the y entry comes before opacity, which changes no value.
It would also help if verify-motion.mjs compared the emitted tweens property by property against
the get_motion_context keyframes, since a frame-energy score cannot see a swapped track.

Activity

  1. jrusso1020 commented on Oct 30, 2026

    @jrusso1020
    Collaborator

    Confirmed on main. In packages/core/src/figma/motionContextToDocs.ts the key lookups have no left boundary (arrayAfterKey line 88, scalarAfterKey line 104, and the indexOf in balancedBlock line 71 used at line 153), so y matches the tail of opacity and copies its track. #6800 and #6810 both fix this; we will pick one and close the other. Comparing tweens property by property in verify-motion.mjs is a good separate improvement.

  2. added
    bugSomething isn't working
    triage/readyAccepted scope, current evidence, acceptance criteria, and validation path are recorded.
    difficulty/easyLocalized change with a known approach, limited risk, and straightforward verification.
    on Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdifficulty/easyLocalized change with a known approach, limited risk, and straightforward verification.triage/readyAccepted scope, current evidence, acceptance criteria, and validation path are recorded.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions