{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glyph-anatomy",
  "title": "Glyph Anatomy",
  "description": "A word builds as in a font editor, with metric guides, anchors along each contour and Bézier handles that pull straight segments into curves before the glyphs fill.",
  "dependencies": ["remotion", "opentype.js", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/glyph-anatomy/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/Inter\";\nimport { type Font, parse } from \"opentype.js\";\nimport { useEffect, useState } from \"react\";\nimport {\n  continueRender,\n  delayRender,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst { fontFamily: INTER } = loadFont(\"normal\", {\n  weights: [\"500\"],\n  subsets: [\"latin\"],\n});\n\nexport interface GlyphAnatomyProps {\n  text?: string;\n  fontUrl?: string;\n  fontSize?: number;\n  color?: string;\n  guideColor?: string;\n  guides?: boolean;\n  keepPoints?: boolean;\n  speed?: number;\n  className?: string;\n}\n\n/** Static Inter ExtraBold (800) from Google Fonts. */\nexport const interExtraBoldTtf =\n  \"https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuDyYMZg.ttf\";\n\nexport const glyphAnatomyDefaults = {\n  text: \"Glyph\",\n  fontUrl: interExtraBoldTtf,\n  fontSize: 200,\n  color: \"#fafafa\",\n  guideColor: \"#0d99ff\",\n  guides: true,\n  keepPoints: false,\n  speed: 1,\n} satisfies Required<Omit<GlyphAnatomyProps, \"className\">>;\n\n/** Beats on the 30 fps clock. Letter beats shift by `letterStagger × i`. */\nexport const glyphAnatomyTiming = {\n  guideStart: 0,\n  guideStagger: 3,\n  guideDraw: 10,\n  anchors: [8, 22],\n  anchorPop: 4,\n  pull: [18, 40],\n  pullDuration: 8,\n  fill: [40, 50],\n  fade: [46, 58],\n  letterStagger: 2,\n} as const;\n\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nconst easeOutCubic = Easing.out(Easing.cubic);\nconst easeInOutCubic = Easing.inOut(Easing.cubic);\nconst popEase = Easing.out(Easing.back(1.7));\nconst pullEase = Easing.out(Easing.back(1.6));\n\nfunction finite(value: number | undefined, fallback: number) {\n  return typeof value === \"number\" && Number.isFinite(value) ? value : fallback;\n}\n\n/** Maps a composition frame onto the 30 fps timeline, scaled by `speed`. */\nexport function getGlyphAnatomyTime(\n  frame: number,\n  options: { fps?: number; speed?: number } = {},\n) {\n  const fps = Math.max(1, finite(options.fps, 30));\n  const speed = Math.max(0, finite(options.speed, 1));\n  return (Math.max(0, finite(frame, 0)) * 30 * speed) / fps;\n}\n\nexport interface Point {\n  x: number;\n  y: number;\n}\n\nexport interface AnatomySegment {\n  from: Point;\n  to: Point;\n  /** Cubic handles; absent on straight segments. */\n  c1?: Point;\n  c2?: Point;\n}\n\nexport interface PathCommand {\n  type: string;\n  x?: number;\n  y?: number;\n  x1?: number;\n  y1?: number;\n  x2?: number;\n  y2?: number;\n}\n\nconst lerp = (a: Point, b: Point, k: number): Point => ({\n  x: a.x + (b.x - a.x) * k,\n  y: a.y + (b.y - a.y) * k,\n});\n\n/**\n * Turns glyph path commands into contours of explicit segments. Quadratic\n * curves become cubics (handles at ⅔ toward the control point), so every\n * curve shows one handle on each end, as in Figma or Glyphs.\n */\nexport function getAnatomyContours(\n  commands: readonly PathCommand[],\n): AnatomySegment[][] {\n  const contours: AnatomySegment[][] = [];\n  let segments: AnatomySegment[] = [];\n  let start: Point | null = null;\n  let at: Point | null = null;\n  const close = () => {\n    if (start && at && (at.x !== start.x || at.y !== start.y)) {\n      segments.push({ from: at, to: start });\n    }\n    if (segments.length > 0) contours.push(segments);\n    segments = [];\n    start = null;\n    at = null;\n  };\n  for (const c of commands) {\n    const to = { x: c.x ?? 0, y: c.y ?? 0 };\n    if (c.type === \"M\") {\n      close();\n      start = to;\n      at = to;\n    } else if (!at) {\n    } else if (c.type === \"L\") {\n      segments.push({ from: at, to });\n      at = to;\n    } else if (c.type === \"Q\") {\n      const q = { x: c.x1 ?? 0, y: c.y1 ?? 0 };\n      segments.push({\n        from: at,\n        to,\n        c1: lerp(at, q, 2 / 3),\n        c2: lerp(to, q, 2 / 3),\n      });\n      at = to;\n    } else if (c.type === \"C\") {\n      segments.push({\n        from: at,\n        to,\n        c1: { x: c.x1 ?? 0, y: c.y1 ?? 0 },\n        c2: { x: c.x2 ?? 0, y: c.y2 ?? 0 },\n      });\n      at = to;\n    } else if (c.type === \"Z\") {\n      close();\n    }\n  }\n  close();\n  return contours;\n}\n\n/**\n * How far each handle has travelled from its anchor to its place, 0 → 1 with\n * overshoot. At 0 the curve is its chord.\n */\nexport function getGlyphAnatomyPull(t: number, order: number, total: number) {\n  const { pull, pullDuration } = glyphAnatomyTiming;\n  const span = pull[1] - pull[0] - pullDuration;\n  const start = pull[0] + (total > 1 ? (span * order) / (total - 1) : 0);\n  return interpolate(t, [start, start + pullDuration], [0, 1], {\n    ...CLAMP,\n    easing: pullEase,\n  });\n}\n\n/** Scale of anchor `order` of `total` popping in along the letter's path. */\nexport function getGlyphAnatomyAnchor(t: number, order: number, total: number) {\n  const { anchors, anchorPop } = glyphAnatomyTiming;\n  const span = anchors[1] - anchors[0] - anchorPop;\n  const start = anchors[0] + (total > 1 ? (span * order) / (total - 1) : 0);\n  return Math.max(\n    0,\n    interpolate(t, [start, start + anchorPop], [0, 1], {\n      ...CLAMP,\n      easing: popEase,\n    }),\n  );\n}\n\n/** Letter-level fades: outline in, fill in, scaffolding out. */\nexport function getGlyphAnatomyLetter(t: number) {\n  const { anchors, fill, fade } = glyphAnatomyTiming;\n  return {\n    outline: interpolate(t, [anchors[0], anchors[0] + 4], [0, 1], CLAMP),\n    fill: interpolate(t, [...fill], [0, 1], {\n      ...CLAMP,\n      easing: easeInOutCubic,\n    }),\n    scaffold: interpolate(t, [...fade], [1, 0], {\n      ...CLAMP,\n      easing: easeInOutCubic,\n    }),\n  };\n}\n\n/** Draw progress of guide `index`, 0..1. */\nexport function getGlyphAnatomyGuide(t: number, index: number) {\n  const { guideStart, guideStagger, guideDraw } = glyphAnatomyTiming;\n  const start = guideStart + guideStagger * index;\n  return interpolate(t, [start, start + guideDraw], [0, 1], {\n    ...CLAMP,\n    easing: easeOutCubic,\n  });\n}\n\n/** Frame where the scaffolding of the last of `count` letters has faded. */\nexport function getGlyphAnatomyLength(count: number) {\n  const { fade, letterStagger } = glyphAnatomyTiming;\n  return fade[1] + letterStagger * Math.max(0, count - 1);\n}\n\n/** Path data for a contour with each handle pulled `k(segment)` of the way. */\nexport function getAnatomyPath(\n  segments: readonly AnatomySegment[],\n  pullOf: (index: number) => number,\n) {\n  if (segments.length === 0) return \"\";\n  const f = (p: Point) => `${p.x.toFixed(2)} ${p.y.toFixed(2)}`;\n  let d = `M${f(segments[0].from)}`;\n  segments.forEach((s, i) => {\n    if (s.c1 && s.c2) {\n      const k = pullOf(i);\n      d += `C${f(lerp(s.from, s.c1, k))} ${f(lerp(s.to, s.c2, k))} ${f(s.to)}`;\n    } else {\n      d += `L${f(s.to)}`;\n    }\n  });\n  return `${d}Z`;\n}\n\ninterface AnatomyLetter {\n  contours: AnatomySegment[][];\n}\n\nfunction layout(font: Font, text: string, fontSize: number) {\n  const scale = fontSize / font.unitsPerEm;\n  // Per-character lookup: full shaping runs GSUB lookups that opentype.js\n  // does not support in every font.\n  const glyphs = Array.from(text).map((char) => font.charToGlyph(char));\n  let x = 0;\n  const letters: AnatomyLetter[] = glyphs.map((glyph, i) => {\n    if (i > 0) x += font.getKerningValue(glyphs[i - 1], glyph) * scale;\n    const contours = getAnatomyContours(glyph.getPath(x, 0, fontSize).commands);\n    x += (glyph.advanceWidth ?? 0) * scale;\n    return { contours };\n  });\n  const os2 = font.tables.os2 as\n    | { sCapHeight?: number; sxHeight?: number }\n    | undefined;\n  const em = (v: number | undefined, fallback: number) =>\n    ((v ?? fallback * font.unitsPerEm) * fontSize) / font.unitsPerEm;\n  return {\n    letters,\n    width: x,\n    capHeight: em(os2?.sCapHeight, 0.7),\n    xHeight: em(os2?.sxHeight, 0.5),\n    descender: (-font.descender * fontSize) / font.unitsPerEm,\n  };\n}\n\nconst fontCache = new Map<string, Promise<Font>>();\n\nfunction loadAnatomyFont(url: string) {\n  let pending = fontCache.get(url);\n  if (!pending) {\n    pending = fetch(url)\n      .then((res) => res.arrayBuffer())\n      .then((buffer) => parse(buffer));\n    fontCache.set(url, pending);\n    pending.catch(() => fontCache.delete(url));\n  }\n  return pending;\n}\n\nfunction useAnatomyFont(url: string) {\n  const [font, setFont] = useState<Font | null>(null);\n\n  useEffect(() => {\n    const handle = delayRender(`glyph-anatomy: loading ${url}`);\n    let released = false;\n    const release = () => {\n      if (released) return;\n      released = true;\n      continueRender(handle);\n    };\n    loadAnatomyFont(url).then((parsed) => {\n      if (!released) setFont(parsed);\n      release();\n    }, release);\n    return release;\n  }, [url]);\n\n  return font;\n}\n\nexport function GlyphAnatomy({\n  text = glyphAnatomyDefaults.text,\n  fontUrl = glyphAnatomyDefaults.fontUrl,\n  fontSize = glyphAnatomyDefaults.fontSize,\n  color = glyphAnatomyDefaults.color,\n  guideColor = glyphAnatomyDefaults.guideColor,\n  guides = glyphAnatomyDefaults.guides,\n  keepPoints = glyphAnatomyDefaults.keepPoints,\n  speed = glyphAnatomyDefaults.speed,\n  className,\n}: GlyphAnatomyProps) {\n  const frame = useCurrentFrame();\n  const { fps, width, height } = useVideoConfig();\n  const font = useAnatomyFont(fontUrl);\n  if (!font) return null;\n\n  const unit = height / 720;\n  const t = getGlyphAnatomyTime(frame, { fps, speed });\n  const word = layout(font, text, fontSize);\n  const originX = (width - word.width * unit) / 2;\n  const baseline = (height + word.capHeight * unit) / 2;\n  const hair = 1.25;\n  const anchorSize = 6;\n  const handleRadius = 2.6;\n  const margin = 0.3 * fontSize;\n  // Guides fade with the last letter's scaffolding.\n  const lastStart =\n    glyphAnatomyTiming.letterStagger * Math.max(0, word.letters.length - 1);\n  const scaffoldAll = keepPoints\n    ? 1\n    : getGlyphAnatomyLetter(t - lastStart).scaffold;\n\n  const guideLines = [\n    { label: \"Cap height\", y: -word.capHeight },\n    { label: \"x-height\", y: -word.xHeight },\n    { label: \"Baseline\", y: 0 },\n    { label: \"Descender\", y: word.descender },\n  ];\n\n  return (\n    <div\n      className={className}\n      style={{ position: \"absolute\", inset: 0, overflow: \"hidden\" }}\n    >\n      <svg\n        width=\"100%\"\n        height=\"100%\"\n        viewBox={`0 0 ${width} ${height}`}\n        style={{ display: \"block\" }}\n      >\n        <g transform={`translate(${originX} ${baseline}) scale(${unit})`}>\n          {guides\n            ? guideLines.map((guide, i) => {\n                const draw = getGlyphAnatomyGuide(t, i);\n                if (draw <= 0) return null;\n                const length = word.width + 2 * margin;\n                return (\n                  <g key={guide.label} opacity={0.7 * scaffoldAll}>\n                    <line\n                      x1={-margin}\n                      x2={-margin + length * draw}\n                      y1={guide.y}\n                      y2={guide.y}\n                      stroke={guideColor}\n                      strokeWidth={hair * 0.8}\n                    />\n                    <text\n                      x={-margin}\n                      y={guide.y - 5}\n                      fill={guideColor}\n                      fontFamily={INTER}\n                      fontSize={11}\n                      fontWeight={500}\n                      opacity={draw}\n                    >\n                      {guide.label}\n                    </text>\n                  </g>\n                );\n              })\n            : null}\n          {word.letters.map((letter, i) => {\n            const lt = t - glyphAnatomyTiming.letterStagger * i;\n            const state = getGlyphAnatomyLetter(lt);\n            const scaffold = keepPoints ? 1 : state.scaffold;\n            const curves = letter.contours.flatMap((segments, c) =>\n              segments.flatMap((s, k) => (s.c1 ? [`${c}:${k}`] : [])),\n            );\n            const curveOrder = new Map(curves.map((key, n) => [key, n]));\n            const anchors = letter.contours.flatMap((segments) =>\n              segments.map((s) => s.from),\n            );\n            const pullOf = (c: number) => (k: number) =>\n              getGlyphAnatomyPull(\n                lt,\n                curveOrder.get(`${c}:${k}`) ?? 0,\n                curves.length,\n              );\n            const paths = letter.contours.map((segments, c) =>\n              getAnatomyPath(segments, pullOf(c)),\n            );\n            let anchorIndex = 0;\n            return (\n              <g key={i}>\n                {state.fill > 0 ? (\n                  <path d={paths.join(\"\")} fill={color} opacity={state.fill} />\n                ) : null}\n                {state.outline > 0 && scaffold > 0 ? (\n                  <g opacity={scaffold}>\n                    {paths.map((d, c) => (\n                      <path\n                        key={c}\n                        d={d}\n                        fill=\"none\"\n                        stroke={color}\n                        strokeWidth={hair}\n                        opacity={state.outline}\n                      />\n                    ))}\n                    {letter.contours.map((segments, c) =>\n                      segments.map((s, k) => {\n                        if (!s.c1 || !s.c2) return null;\n                        const pull = pullOf(c)(k);\n                        if (pull <= 0) return null;\n                        const h1 = lerp(s.from, s.c1, pull);\n                        const h2 = lerp(s.to, s.c2, pull);\n                        return (\n                          <g key={`${c}-${k}`}>\n                            <line\n                              x1={s.from.x}\n                              y1={s.from.y}\n                              x2={h1.x}\n                              y2={h1.y}\n                              stroke={guideColor}\n                              strokeWidth={hair * 0.8}\n                            />\n                            <line\n                              x1={s.to.x}\n                              y1={s.to.y}\n                              x2={h2.x}\n                              y2={h2.y}\n                              stroke={guideColor}\n                              strokeWidth={hair * 0.8}\n                            />\n                            <circle\n                              cx={h1.x}\n                              cy={h1.y}\n                              r={handleRadius}\n                              fill={guideColor}\n                            />\n                            <circle\n                              cx={h2.x}\n                              cy={h2.y}\n                              r={handleRadius}\n                              fill={guideColor}\n                            />\n                          </g>\n                        );\n                      }),\n                    )}\n                    {letter.contours.map((segments, c) =>\n                      segments.map((s, k) => {\n                        const scale = getGlyphAnatomyAnchor(\n                          lt,\n                          anchorIndex++,\n                          anchors.length,\n                        );\n                        if (scale <= 0) return null;\n                        const size = anchorSize * scale;\n                        return (\n                          <rect\n                            key={`${c}-${k}`}\n                            x={s.from.x - size / 2}\n                            y={s.from.y - size / 2}\n                            width={size}\n                            height={size}\n                            fill=\"#ffffff\"\n                            stroke={guideColor}\n                            strokeWidth={hair}\n                          />\n                        );\n                      }),\n                    )}\n                  </g>\n                ) : null}\n              </g>\n            );\n          })}\n        </g>\n      </svg>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/glyph-anatomy.tsx"
    }
  ],
  "type": "registry:component"
}
