Skip to content

feat(registry): add morph-text component and text-effects catalog section#1247

Merged
vanceingalls merged 7 commits into
mainfrom
06-06-feat_registry_add_morph-text_component
Jun 7, 2026
Merged

feat(registry): add morph-text component and text-effects catalog section#1247
vanceingalls merged 7 commits into
mainfrom
06-06-feat_registry_add_morph-text_component

Conversation

@vanceingalls
Copy link
Copy Markdown
Collaborator

@vanceingalls vanceingalls commented Jun 6, 2026

What

Adds a new morph-text component 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

  • Gooey SVG threshold morph that cycles through an editable list of statements
  • GSAP seekable proxy pattern (tween on proxy.t, onUpdate → applyMorph) for frame-accurate rendering
  • SVG feColorMatrix threshold filter only applied during active morph — text is crisp at rest
  • drop-shadow on wrapper (outside the threshold filter) so shadow renders through the morph
  • Figtree 900, black text, white background

Text Effects catalog section (Studio)

  • New text-effects BlockCategory in @hyperframes/core with violet color
  • text-effect tag resolver in resolveBlockCategory (checked before generic effect tag)
  • caption-blend-difference, texture-mask-text, morph-text tagged with text-effect
  • Studio useBlockCatalog and blockCategories updated with order and color

Docs (Mintlify)

  • New "Text Effects" group in docs/docs.json below "Effects"
  • Moved caption-blend-difference and texture-mask-text out of "Effects" into "Text Effects"
  • New docs/catalog/components/morph-text.mdx page with video preview and install instructions

Test plan

  • hyperframes add morph-text installs correctly
  • Studio catalog shows Text Effects section below Effects
  • All three text-effect components appear in the section
  • morph-text renders and seeks correctly in Studio
  • Mintlify site shows Text Effects group with all three components

miguel-heygen and others added 2 commits June 6, 2026 15:04
…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>
Copy link
Copy Markdown
Collaborator Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@vanceingalls vanceingalls changed the title feat(gsap): add innerText support to GSAP inspector for counter animations (#1244) feat(registry): add morph-text component and text-effects catalog section Jun 6, 2026
vanceingalls and others added 4 commits June 6, 2026 15:07
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>
Copy link
Copy Markdown

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_CATEGORIES extended with text-effects (violet) in packages/core/src/registry/types.ts:198. ✓
  • resolveBlockCategory checks text-effect before effect (lines 212-213). ✓ Correct precedence — otherwise the new section would never receive items.
  • Studio useBlockCatalog CATEGORY_ORDER positions text-effects at 4 (between effects and social). ✓ Matches the screenshot's section ordering.
  • getCategoryColors map updated with violet for text-effects. ✓
  • Mintlify docs/docs.json moves caption-blend-difference + texture-mask-text out of "Effects" and into the new "Text Effects" group alongside morph-text. ✓ Path moves are pure relocations (no page slug renames), so existing inbound links to catalog/components/caption-blend-difference survive.
  • registry/registry.json adds morph-text to the components list. ✓

For morph-text.html (the bulk of the diff):

  • GSAP timeline uses the seekable proxy.t + onUpdate → applyMorph pattern, registered to window.__timelines["morph-text"]. ✓ Matches the HF-native pattern that the probe/render path expects (probeStage looks for window.__timelines[*]).
  • SVG feColorMatrix threshold filter is only applied during active morph (frac > 0 && frac < 1); at rest, display.style.filter = "none" → text renders crisply through the drop-shadow wrapper. ✓ Nice touch — this avoids the common "GPU-eating filter always-on" pattern.
  • applyMorph(T) does T % totalWordDur so cycling wraps cleanly. ✓
  • Word-list defaulting: if fewer than 2 <li> items, falls back to [Morph, Text]. ✓ Defensive shape.
  • data-timeline-locked is 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-motion is 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"> + a fonts.googleapis.com link. When a user runs hyperframes add morph-text and 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.json to include GSAP + Figtree as bundled files (the files array supports multiple paths with target destinations).
    • (b) Or, at minimum, document the network requirement on the .mdx page 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.
  • The two re-tagged components had their effect tag 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_added events emit tags: [...] as a property and the team has saved queries / funnels keyed on tag = "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 resolveBlockCategory priority order would still work correctly with both tags present (line 212 catches text-effect first; the effect fallback at line 213 never fires for them). Restoring effect as 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 small frac clamps the upper end, but at frac near 1 the formula goes negative (e.g. at frac = 0.99, 8/0.01 - 8 = 792 is fine, but 8/(1-0.99) - 8 = 792 also fine — but at frac = 0.5, 8/0.5 - 8 = 8; at edges I missed). Re-checking: actually the formula is blurB = 8/frac - 8 which goes from infinity at frac→0 (clamped to 100) down to 0 at frac = 1; and blurA = 8/(1-frac) - 8 from infinity at frac→1 down to 0 at frac = 0. Neither goes negative inside (0, 1). False alarm — the math is symmetric and well-behaved. Still, an explicit Math.max(0, ...) would make intent clearer to the next reader. (nit)
  • Two sources of truth for category order. BLOCK_CATEGORIES in core/registry/types.ts:193 is already an ordered array; CATEGORY_ORDER in useBlockCatalog.ts:18 re-declares the order as a record. They'll drift the next time someone reorders. The studio hook could just BLOCK_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 their textContent rewritten every morph step; a screen reader announcing this could rattle through all 5 statements rapidly. Two reasonable shapes: (a) add aria-live="polite" + an aria-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=block means 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 on document.fonts.ready would be defensive:
    document.fonts.ready.then(() => {
      window.__timelines["morph-text"] = tl;
    });
    ...though HF's probe stage explicitly waits for fonts ready (probeStage logs fonts 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 effect tag intentional removed from caption-blend-difference and texture-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 picks text-effects first.
  • Is there a HF-wide stance on prefers-reduced-motion in 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-locked on 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 green Render catalog previews CI check.
  • The actual Render catalog previews output 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 = 0 exactly vs frac = 0.0000001 due to float arithmetic) — the conditional branches at frac <= 0 and frac >= 1 look correct.
  • PostHog event-property shape for catalog clicks. Flagged based on the delegation briefing's prompt, not on direct verification.
  • demo.html contents vs morph-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

Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@vanceingalls
Copy link
Copy Markdown
Collaborator Author

Thanks Rames and Miguel — addressed everything:

Tag replacement (high risk) — restored "effect" on both caption-blend-difference and texture-mask-text so they carry both text-effect and effect. resolveBlockCategory still picks text-effect first via precedence; analytics/tag-equality searches on "effect" continue to match.

morphPause fallback — aligned to "1.5" to match the data-morph-pause attribute default.

Blur clamp — added Math.max(0, ...) alongside the existing Math.min cap.

prefers-reduced-motion — added early return before timeline registration: skips the morph, renders first word statically, doesn't call back into applyMorph. Render pipeline is unaffected (runs headless without user-agent motion settings); studio catalog browsing now respects OS preference.

CATEGORY_ORDER drift — removed the hardcoded Record<BlockCategory, number> from useBlockCatalog; sort now derives order from BLOCK_CATEGORIES.findIndex((c) => c.id === item.category). BLOCK_CATEGORIES in core/registry/types.ts is the single source of truth.

demo.html — added a comment at the top explaining it's used exclusively by scripts/generate-catalog-previews.ts for the catalog preview render and is not installed by hyperframes add morph-text.

CDN GSAP + Google Fonts — not changed in this PR. Other catalog components (grain-overlay, shimmer-sweep, all the caption-* components) also use CDN GSAP; bundling it is a repo-wide concern better tracked separately. morph-text.html has a note in the install docs that network access is required for the font.

data-timeline-locked — pre-existing attribute; used by Studio to signal the timeline duration is managed by the component and shouldn't be user-editable via drag/trim. Not new here.

@vanceingalls vanceingalls merged commit 48fcf4a into main Jun 7, 2026
51 of 82 checks passed
@vanceingalls vanceingalls deleted the 06-06-feat_registry_add_morph-text_component branch June 7, 2026 17:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants