Every SKILL.md already declared the new name — name: frames, frames-cli,
frames-core, and so on — but the directories were never moved, so the tree
carried frames-* in content and hyperframes-* on disk. Everything that
resolves a skill by path missed: the core-skill list, the plugin marketplace
pin, and the docs the CLI reads at runtime. Eight tests failed on main because
the files they open do not exist.
The directories now match what the files say they are, and the guide the
claude-design contract reads takes the name that contract already used.
A free-floating container morphs apparent size, corner radius, and surface treatment between two shots — the morph itself IS the transition; the viewer's eye tracks the persistent container. Distinct from anchored-layout-expand.md (an edge-pinned live layout participant that grows along one axis and reflows neighbors — here nothing is pushed) and theme-crossfade-morph.md (a whole-theme reskin under a fixed anchor — here a single container changes shape).
How It Works
Since width/height tweens are forbidden, substitute uniform scale for apparent size; the remaining morph channels are paint-only: borderRadius, background, boxShadow. All channels ride ONE tween (one ease, one duration) so the shape morphs in lockstep. Content choreography: old content fades out during the first ~40% of the morph, new content fades in during the last ~40% — the shape-only gap between is the natural "blink." Optionally the morph card itself fades at the very end, revealing the real next-shot element rendered behind it.
Recipe
<!-- inside a standard scene clip (frames-core) --><!-- DOM order = stacking: the anchor renders BEFORE the card, so the card is on top --><divclass="next-shot-anchor"><imgsrc="{nextShotAnchor}"alt="anchor"/></div><divclass="morph-card"><divclass="content-old">{shotOneContent}</div><divclass="content-new">{shotTwoContent}</div></div>
.morph-card{width:SHOT_ONE_W;height:SHOT_ONE_H;/* shot-1 geometry; the morph is scale, never width/height */border-radius:SHOT_ONE_RADIUS;background:{surfaceShotOne};overflow:hidden;/* content must clip during the shape change */display:grid;place-items:center;will-change:transform;}.content-old,.content-new{position:absolute;inset:0;display:grid;place-items:center;}.content-new{opacity:0;/* author its inner sizes at apparent-size ÷ END_SCALE — it scales with the card */}.next-shot-anchor{position:absolute;opacity:0;/* fades in as the morph card fades out */}
constEND_SCALE=SHOT_TWO_W/SHOT_ONE_W;// uniform — keep the two shots aspect-matched
// Hold shot 1 for HOLD_BEAT first — an instant morph reads as glitchy.
// One tween, all channels: uniform scale + paint-only properties.
tl.to(".morph-card",{scale:END_SCALE,borderRadius:SHOT_TWO_RADIUS/END_SCALE,// borderRadius is pre-scale — divide to land the APPARENT radius
background:"{surfaceShotTwo}",boxShadow:"{shadowShotTwo}",duration:MORPH_DUR,ease:"power2.inOut",},MORPH_START,);tl.to(".content-old",{opacity:0,duration:MORPH_DUR*OLD_FADE_FRAC,ease:"power1.in"},MORPH_START,);tl.to(".content-new",{opacity:1,duration:MORPH_DUR*NEW_FADE_FRAC,ease:"power1.out"},MORPH_START+MORPH_DUR*(1-NEW_FADE_FRAC),);// Optional handoff — card fades out over the pixel-identical real anchor.
tl.to(".morph-card",{opacity:0,duration:MORPH_DUR*FINAL_FADE_FRAC,ease:"power1.in",immediateRender:false},MORPH_START+MORPH_DUR*(1-FINAL_FADE_FRAC),);tl.to(".next-shot-anchor",{opacity:1,duration:MORPH_DUR*FINAL_FADE_FRAC,ease:"power1.out"},MORPH_START+MORPH_DUR*(1-FINAL_FADE_FRAC),);
Morph channels
channel
how
apparent size
uniform scale — the substitution for the forbidden width/height tween; aspect preserved
borderRadius
paint-only; pre-scale units — tween to APPARENT_RADIUS / END_SCALE, ≤ half the smaller side
background
paint-only; gradients interpolate only with equal stop counts (solid→solid: backgroundColor)
boxShadow
paint-only; base shadow → accent glow shifts emphasis
Variations
Landing on a non-centered target (dock icon, sidebar slot): add x/y to the same tween, computed as the FLIP-style delta between the card's and the target's rects — getBoundingClientRect() both at build time (single-scene only, per the contract) and tween the difference. Don't hand-compute from CSS values: paddings, borders, and parent transforms compound, and center-vs-edge arithmetic is the classic off-by-half bug.
Aspect change between shots: uniform scale preserves aspect — morph to the nearest uniform fit and let the crossfade/handoff absorb the small delta, or drop the handoff and hold the card's final state.
Values
token
range
notes
HOLD_BEAT
0.6–1.5s
≥ shot 1's entry settle; the viewer must register shot 1 first
MORPH_DUR
0.6–1.2s
< 0.5s can't fit both content fades
END_SCALE
SHOT_TWO_W / SHOT_ONE_W
icon-sized handoffs typically land at 80–400px apparent width
SHOT_TWO_RADIUS
≤ min(W, H)/2 apparent
half the smaller side = perfect circle; beyond is clamped
OLD/NEW_FADE_FRAC
0.3–0.5 each, sum ≤ 1
the gap between is the shape-only "blink"
FINAL_FADE_FRAC
0 (no handoff) or 0.1–0.2
only when a pixel-identical anchor exists
ease
power2.inOut canonical
power3/expo.inOut OK; never back/elastic — overshoot fights the shape change
Critical Constraints
❗ Uniform-scale substitution — never tween width/height; scale + the paint-only channels (borderRadius, background, boxShadow) are the ONLY morph properties.
❗ Handoff anchor must be pixel-identical to the card's final state — same apparent size, radius, background, shadow, inner icon dimensions. Any delta = a visible pop during the crossfade. Can't match exactly? Drop the handoff and hold the morph card.
❗ Stacking by DOM order, never a z-index snap mid-fade — render the anchor before the card; a tl.set({ zIndex }) during an active opacity tween flips stacking before the fade finishes and flickers.
overflow: hidden on the card — content must clip as the radius changes.
Hold a beat before morphing; same ease family for shape and crossfade (mixed eases read unsynchronized).
See also
anchored-layout-expand (edge-pinned one-axis growth with reflow) · theme-crossfade-morph (whole-theme reskin under a fixed anchor) · scale-swap-transition (content swap without shape change) · sine-wave-loop (a breath on the final state).