feat(registry): add morph-text component and text-effects catalog section#1247
Conversation
…tions (#1244) Adds 'innerText' as a supported GSAP property so number roll-up animations (count-up from 0 to some value) are visible and editable in the GSAP inspector panel. - Add 'innerText' to SUPPORTED_PROPS in gsapConstants.ts - Add label 'Counter Value', tooltip, and step constraint (1) in gsapAnimationConstants.ts The snap modifier that controls integer rounding is already preserved verbatim via the EXTRAS_KEYS round-trip, so rounding behavior survives edits without any additional UI changes. Closes #1179
…nent Introduces a new "Text Effects" catalog section (below Effects) for text-focused visual components. - Add `text-effects` BlockCategory to core registry types with violet color - Add `text-effect` tag resolver in resolveBlockCategory (checked before generic `effect` tag) - Tag caption-blend-difference, texture-mask-text, and morph-text with `text-effect` - Update studio catalog order and color map to include text-effects - Add morph-text component: gooey SVG threshold morph cycling through editable statements using GSAP seekable proxy pattern for deterministic/seekable rendering Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This stack of pull requests is managed by Graphite. Learn more about stacking. |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves caption-blend-difference and texture-mask-text out of Effects into a new "Text Effects" section below it. Adds morph-text component page with install instructions and preview video. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Hey Vance — clean catalog addition with the new "Text Effects" section, and the morph component itself is well-built (GSAP seekable proxy pattern, threshold filter only when mid-morph so text is crisp at rest, sensible word-list defaulting). No blockers. A couple of HF-lens concerns about motion accessibility + CDN assets, and one analytics-shaped catch in how the two re-tagged components were touched.
What I verified
Walked the catalog wiring end-to-end:
BLOCK_CATEGORIESextended withtext-effects(violet) inpackages/core/src/registry/types.ts:198. ✓resolveBlockCategorycheckstext-effectbeforeeffect(lines 212-213). ✓ Correct precedence — otherwise the new section would never receive items.- Studio
useBlockCatalogCATEGORY_ORDER positions text-effects at 4 (between effects and social). ✓ Matches the screenshot's section ordering. getCategoryColorsmap updated with violet for text-effects. ✓- Mintlify
docs/docs.jsonmovescaption-blend-difference+texture-mask-textout of "Effects" and into the new "Text Effects" group alongsidemorph-text. ✓ Path moves are pure relocations (no page slug renames), so existing inbound links tocatalog/components/caption-blend-differencesurvive. registry/registry.jsonaddsmorph-textto the components list. ✓
For morph-text.html (the bulk of the diff):
- GSAP timeline uses the seekable
proxy.t+onUpdate → applyMorphpattern, registered towindow.__timelines["morph-text"]. ✓ Matches the HF-native pattern that the probe/render path expects (probeStage looks forwindow.__timelines[*]). - SVG
feColorMatrixthreshold filter is only applied during active morph (frac > 0 && frac < 1); at rest,display.style.filter = "none"→ text renders crisply through thedrop-shadowwrapper. ✓ Nice touch — this avoids the common "GPU-eating filter always-on" pattern. applyMorph(T)doesT % totalWordDurso cycling wraps cleanly. ✓- Word-list defaulting: if fewer than 2
<li>items, falls back to[Morph, Text]. ✓ Defensive shape. data-timeline-lockedis set on the root — assuming this signals to the studio editor that the timeline duration is fixed by the component, not user-editable. (See Question 3.)
Concerns
-
prefers-reduced-motionis not respected. A text morph with blur + threshold crossfade is a textbook vestibular trigger — exactly the shape of animation that motion-sensitive users get nauseous from. The current implementation autoplays the timeline regardless of OS-level motion preference. For a catalog example that ships as the canonical "morph text" implementation, this is also the right place to set the HF-wide pattern. Cheapest fix at the component layer:var reduce = matchMedia("(prefers-reduced-motion: reduce)").matches; if (reduce) { // Skip the morph; just show the first word. elA.textContent = words[0].text; elA.style.fontFamily = words[0].font; elA.style.color = words[0].color; elA.style.opacity = "1"; display.style.filter = "none"; return; // don't register the timeline }
Or, if HF compositions are expected to always play through (render pipeline doesn't care about user-agent motion settings), this matters specifically for the studio preview path — catalog browsing shouldn't induce motion sickness while a designer is scrolling through 30 components.
-
CDN-loaded GSAP + Google Fonts in a catalog component conflicts with HF's own anti-pattern guidance. The probe stage in #1236's diagnostic emits this exact hint: "Bundle GSAP locally in your project instead of using a CDN
<script src>." This catalog component ships with<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js">+ afonts.googleapis.comlink. When a user runshyperframes add morph-textand tries to render on a low-connectivity / cgroup-restricted box, the CDN fetch fails → renders silently break with "Composition duration is 0" (the same hint surfaces). Two cheap fixes:- (a) Update
registry-item.jsonto include GSAP + Figtree as bundled files (thefilesarray supports multiple paths withtargetdestinations). - (b) Or, at minimum, document the network requirement on the
.mdxpage so users running offline / locked-down hit a clear expectation, not a silent black-frame render. - The texture-mask-text component already bundles its mask assets (44 of them, per the registry-item.json). The pattern exists — morph-text should follow it for GSAP + the font.
- (a) Update
-
The two re-tagged components had their
effecttag REPLACED, not augmented. Concrete diff vs main:caption-blend-difference:["text", "effect", "blend-mode", "contrast", "inversion"]→["text", "text-effect", "blend-mode", "contrast", "inversion"]texture-mask-text:["text", "texture", "mask", "effect"]→["text", "text-effect", "texture", "mask"]
Any downstream consumer that filters by the literal
"effect"tag now silently misses these two:- PostHog / analytics: if
block_addedevents emittags: [...]as a property and the team has saved queries / funnels keyed ontag = "effect", that signal will drop on these two without warning. - Catalog search by tag (if it surfaces tag-based suggestions): typing "effect" no longer matches these two via tag-equality.
- The
resolveBlockCategorypriority order would still work correctly with both tags present (line 212 catchestext-effectfirst; theeffectfallback at line 213 never fires for them). Restoringeffectas a secondary tag costs nothing and keeps the analytics signal intact. Three-character fix.
Nits
- Blur math has negative-blur edges.
Math.min(8/frac - 8, 100)at smallfracclamps the upper end, but atfracnear 1 the formula goes negative (e.g. atfrac = 0.99,8/0.01 - 8 = 792is fine, but8/(1-0.99) - 8 = 792also fine — but atfrac = 0.5,8/0.5 - 8 = 8; at edges I missed). Re-checking: actually the formula isblurB = 8/frac - 8which goes from infinity atfrac→0(clamped to 100) down to 0 atfrac = 1; andblurA = 8/(1-frac) - 8from infinity atfrac→1down to 0 atfrac = 0. Neither goes negative inside(0, 1). False alarm — the math is symmetric and well-behaved. Still, an explicitMath.max(0, ...)would make intent clearer to the next reader. (nit) - Two sources of truth for category order.
BLOCK_CATEGORIESincore/registry/types.ts:193is already an ordered array;CATEGORY_ORDERinuseBlockCatalog.ts:18re-declares the order as a record. They'll drift the next time someone reorders. The studio hook could justBLOCK_CATEGORIES.findIndex((c) => c.id === item.category)instead — single source. (nit) - Accessibility around the cycling text.
<ol id="morph-words" aria-hidden="true">is correctly hidden from AT (good — it's the source-of-truth, not what the user "sees"). But the visible<span id="morph-a">/<span id="morph-b">get theirtextContentrewritten every morph step; a screen reader announcing this could rattle through all 5 statements rapidly. Two reasonable shapes: (a) addaria-live="polite"+ anaria-atomic="true"wrapper so only the settled word announces, OR (b)aria-hidden="true"on the visible spans + a single visually-hidden region that names the component once. Out of scope to fix here, but worth a future pass with the a11y lens. (nit) Figtree:wght@900&display=blockmeans text is invisible until the font loads. Good for avoiding FOUT, but if the GSAP timeline starts before fonts are ready, the morph happens with no visible text for the first 100-200ms. Gating timeline registration ondocument.fonts.readywould be defensive:...though HF's probe stage explicitly waits fordocument.fonts.ready.then(() => { window.__timelines["morph-text"] = tl; });
fonts ready(probeStage logsfonts ready), so render-time is covered; this would only matter for studio live-preview. (nit)- The 100px max blur cap is hand-tuned for 120pt text. A smaller text size + the same cap would produce visually different proportional blur. Probably fine in practice; just flagging that the values are content-specific. (nit)
Questions
- Was the
effecttag intentional removed fromcaption-blend-differenceandtexture-mask-text, or accidental during the re-tag? If intentional, what's the rationale for the tag exclusivity? My read: keeping both tags is the safer default and preserves any analytics signal — the resolver still pickstext-effectsfirst. - Is there a HF-wide stance on
prefers-reduced-motionin catalog components? Asking because either (a) HF intentionally always plays animations and motion-sensitive users opt out at a higher layer, or (b) no stance exists yet and this is a good place to set one. Worth a one-line decision in the PR body either way. data-timeline-lockedon the root element — is this new, or did it exist before? Couldn't find another component using it in a quick grep (didn't exhaustively search). If new, worth a comment near the attribute explaining what it tells the studio editor.- Studio search: typing "effect" — does it match by tag, category label, description, or all of them? Affects whether the re-tag drops these two from search results.
What I didn't verify
- The demo
.mp4(/tmp/slack-channel-files/1780784861124-...mp4) — Read tool doesn't handle video; trusted the PR description + the greenRender catalog previewsCI check. - The actual
Render catalog previewsoutput for morph-text (CI passed; assumed visual is correct). - Whether any onboarding / tour overlay or saved-search references the old "Effects" section path for the two moved components. Grep would find it; didn't run.
- Cross-machine determinism of the GSAP timeline scrub at frame-boundary times (e.g.
frac = 0exactly vsfrac = 0.0000001due to float arithmetic) — the conditional branches atfrac <= 0andfrac >= 1look correct. - PostHog event-property shape for catalog clicks. Flagged based on the delegation briefing's prompt, not on direct verification.
demo.htmlcontents vsmorph-text.html— both are 225 lines per the diff; assumed they're identical (or near-identical, with demo possibly using slightly different word-list for the preview render).
— Review by Rames D Jusso
miguel-heygen
left a comment
There was a problem hiding this comment.
Building on Rames' review — all three of his concerns are real; the tag-replacement one is the easiest fix with the highest downstream risk. One concrete finding he noted he hadn't verified, and a small script inconsistency:
demo.html is identical to morph-text.html and is not registered
Confirmed byte-for-byte: both files are 225 lines with the same content. More importantly, registry-item.json only lists morph-text.html in the files array — demo.html is an orphan that won't be installed or surfaced by hyperframes add morph-text. If it's used by CI to render the catalog preview video, it should be documented (a comment in the file or a note in the PR); if it's a leftover artifact from dev scaffolding, it can be deleted. Either way, shipping two identical files where one goes nowhere is confusing for future contributors.
data-morph-pause fallback mismatch
The root element sets data-morph-pause="1.5", but the script's fallback is parseFloat(container.dataset.morphPause || "0.25"). When the attribute is present the correct value (1.5) is read — no bug here. But someone extracting just the <script> block into a bare composition without the data attributes gets 0.25s pauses instead of 1.5s, yielding a noticeably different rhythm. Aligning the fallback to "1.5" costs one character and makes the default explicit. (nit)
Amplifying Rames on the tag replacement
This is the one I'd flag as worth fixing before merge. Three-character change per component, zero risk, and it keeps any saved PostHog query or tag-equality search working for "effect". The resolveBlockCategory priority ordering already ensures text-effect wins for Studio categorization — there's no conflict in keeping both.
Otherwise LGTM — the seekable proxy pattern, threshold-only-during-morph, and cycling math are all well-executed.
- Restore `effect` tag on caption-blend-difference and texture-mask-text alongside `text-effect` so existing tag-equality searches/analytics still match - Fix morphPause script fallback from "0.25" to "1.5" to match data attribute default - Add Math.max(0, ...) guard to blur values (intent clarity) - Add prefers-reduced-motion: skip morph and show first word statically - Remove CATEGORY_ORDER record from useBlockCatalog; derive order from BLOCK_CATEGORIES array (single source of truth, no drift) - Add comment to demo.html documenting its purpose (catalog preview script only) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Thanks Rames and Miguel — addressed everything: Tag replacement (high risk) — restored
Blur clamp — added
CDN GSAP + Google Fonts — not changed in this PR. Other catalog components (
|

What
Adds a new
morph-textcomponent to the registry and a new Text Effects catalog section — in Studio, docs, and the Mintlify site.Why
Text-effect components (blend-difference, texture-mask, morph-text) were miscategorized under the generic Effects section. A dedicated Text Effects section makes them easier to discover.
How
morph-text component
tween on proxy.t,onUpdate → applyMorph) for frame-accurate renderingfeColorMatrixthreshold filter only applied during active morph — text is crisp at restdrop-shadowon wrapper (outside the threshold filter) so shadow renders through the morphText Effects catalog section (Studio)
text-effectsBlockCategory in@hyperframes/corewith violet colortext-effecttag resolver inresolveBlockCategory(checked before genericeffecttag)caption-blend-difference,texture-mask-text,morph-texttagged withtext-effectuseBlockCatalogandblockCategoriesupdated with order and colorDocs (Mintlify)
docs/docs.jsonbelow "Effects"caption-blend-differenceandtexture-mask-textout of "Effects" into "Text Effects"docs/catalog/components/morph-text.mdxpage with video preview and install instructionsTest plan
hyperframes add morph-textinstalls correctly