Repository navigation
Description
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
npm pack @hyperframes/[email protected] && tar xzf hyperframes-core-0.8.217.tgz && (cd package && npm install --omit=dev)node repro.mjs ./package- Look at the
ytrack.
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.30Additional 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
jrusso1020 commentedon Oct 30, 2026
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
ymatches the tail ofopacityand 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.