{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "axis-flex",
  "title": "Axis Flex",
  "description": "A gradient word flexes its width and weight like an elastic variable font.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/axis-flex/index.tsx",
      "content": "\"use client\";\n\nimport { loadVariableFont } from \"@remotion/google-fonts/MonaSans\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n  AbsoluteFill,\n  continueRender,\n  delayRender,\n  Easing,\n  interpolate,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst flex = loadVariableFont(\"normal\", { subsets: [\"latin\"] });\n\nexport interface AxisFlexProps {\n  /** The word that flexes. */\n  word?: string;\n  /** Second line, set in the narrow cut under the word. */\n  line?: string;\n  accent?: string;\n  theme?: \"dark\" | \"light\";\n  /** Playback multiplier. */\n  speed?: number;\n}\n\n/**\n * The word is filled with the gradients.sh \"beam\" palette (clip-text), which\n * pairs with the accent; it only drifts a few degrees over the shot.\n */\nconst THEMES = {\n  dark: {\n    bg: \"#0A0A0B\",\n    fill: (accent: string) => [\"#F4F7FF\", \"#C9D8FF\", \"#8FB4FF\", accent],\n  },\n  light: {\n    bg: \"#F5F4F0\",\n    fill: (accent: string) => [\"#1A1660\", \"#2B2A8A\", accent, \"#5E86F0\"],\n  },\n} as const;\n\ninterface Axes {\n  wdth: number;\n  wght: number;\n}\n\n/** Mona Sans axes: wdth 75–125, wght 200–900; the wave stays at 500 or lighter. */\nconst AXIS = { wdth: [75, 125], wght: [200, 500] } as const;\n/** Equalizer floor and ceiling: light condensed up to wide medium. */\nconst LOW: Axes = { wdth: 75, wght: 200 };\nconst HIGH: Axes = { wdth: 125, wght: 500 };\n/** The style every letter snaps to. */\nconst FINAL: Axes = { wdth: 120, wght: 460 };\n/** The narrow cut for the second line. */\nconst NARROW: Axes = { wdth: 75, wght: 400 };\n\nconst TRACK = -0.02;\n\n// Timeline (frames at speed 1).\nconst WAVE_IN = 6;\nconst WAVE_FULL = 22;\nconst PERIOD = 30;\nconst SETTLE = 84;\nconst SETTLE_STAGGER = 2;\nconst LINE_IN = SETTLE + 16;\nconst SAMPLE_END = SETTLE + 24;\n\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\nconst SPRING_SNAP = { damping: 14, stiffness: 170, mass: 0.7 };\n\nconst mix = (a: Axes, b: Axes, k: number): Axes => ({\n  wdth: a.wdth + (b.wdth - a.wdth) * k,\n  wght: a.wght + (b.wght - a.wght) * k,\n});\n\nconst clampAxes = (a: Axes): Axes => ({\n  wdth: Math.min(AXIS.wdth[1], Math.max(AXIS.wdth[0], a.wdth)),\n  wght: Math.min(AXIS.wght[1], Math.max(AXIS.wght[0], a.wght)),\n});\n\nconst fvs = (a: Axes) =>\n  `\"wdth\" ${a.wdth.toFixed(2)}, \"wght\" ${a.wght.toFixed(1)}`;\n\n/** Equalizer level of glyph i at time t: a travelling wave plus a faster ripple. */\nfunction level(i: number, t: number) {\n  const amp = interpolate(t, [WAVE_IN, WAVE_FULL], [0, 1], {\n    ...CLAMP,\n    easing: Easing.bezier(0.45, 0, 0.55, 1),\n  });\n  const ph = ((t - WAVE_IN) / PERIOD) * Math.PI * 2;\n  const main = Math.sin(ph - i * 0.95);\n  const ripple = Math.sin(ph * 1.85 - i * 1.7 + 1.1);\n  const v = (main * 0.72 + ripple * 0.28 + 1) / 2;\n  const shaped = v * v * (3 - 2 * v);\n  return 0.32 + (shaped - 0.32) * amp;\n}\n\nconst LEVELS = [0, 0.25, 0.5, 0.75, 1];\n\ninterface Metrics {\n  /** Advance per glyph (em) at each sampled level. */\n  adv: number[][];\n  finalWidth: number;\n  /** Second line width (em) in the narrow cut. */\n  lineWidth: number;\n}\n\nfunction measure(chars: string[], line: string): Metrics {\n  const probe = document.createElement(\"span\");\n  Object.assign(probe.style, {\n    position: \"absolute\",\n    left: \"-99999px\",\n    top: \"0\",\n    visibility: \"hidden\",\n    whiteSpace: \"pre\",\n    fontFamily: `\"${flex.fontFamily}\"`,\n    fontSize: \"100px\",\n    lineHeight: \"1\",\n  });\n  document.body.appendChild(probe);\n  const width = (s: string, a: Axes) => {\n    probe.style.fontVariationSettings = fvs(a);\n    probe.textContent = s;\n    return probe.getBoundingClientRect().width / 100;\n  };\n  const adv = chars.map((ch) =>\n    LEVELS.map((l) => width(ch, mix(LOW, HIGH, l)) + TRACK),\n  );\n  const finalWidth = chars.reduce((s, ch) => s + width(ch, FINAL) + TRACK, 0);\n  const lineWidth = width(line, NARROW);\n  probe.remove();\n  return { adv, finalWidth, lineWidth };\n}\n\nfunction useMetrics(chars: string[], line: string) {\n  const [metrics, setMetrics] = useState<Metrics | null>(null);\n  const [handle] = useState(() => delayRender(\"axis-flex: measuring\"));\n  const released = useRef(false);\n  useEffect(() => {\n    let live = true;\n    Promise.all([\n      flex.waitUntilDone(),\n      document.fonts.load(`400 100px \"${flex.fontFamily}\"`),\n    ])\n      .catch(() => undefined)\n      .then(() => {\n        if (!live) return;\n        setMetrics(measure(chars, line));\n        if (!released.current) {\n          released.current = true;\n          continueRender(handle);\n        }\n      });\n    return () => {\n      live = false;\n    };\n  }, [chars, line, handle]);\n  return metrics;\n}\n\nfunction advAt(row: number[], l: number) {\n  const x = Math.min(1, Math.max(0, l)) * (LEVELS.length - 1);\n  const k = Math.min(LEVELS.length - 2, Math.floor(x));\n  return row[k] + (row[k + 1] - row[k]) * (x - k);\n}\n\nexport function AxisFlex({\n  word = \"elastic\",\n  line = \"One family, every width\",\n  accent = \"#3B5BFD\",\n  theme = \"dark\",\n  speed = 1,\n}: AxisFlexProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H, fps } = useVideoConfig();\n  const c = THEMES[theme];\n  const chars = useMemo(() => Array.from(word), [word]);\n  const m = useMetrics(chars, line);\n\n  // Widest the row ever gets while flexing, so it never leaves the frame.\n  const widest = useMemo(() => {\n    if (!m) return 1;\n    let max = m.finalWidth;\n    for (let f = 0; f <= SAMPLE_END; f++) {\n      let w = 0;\n      for (let i = 0; i < chars.length; i++) w += advAt(m.adv[i], level(i, f));\n      max = Math.max(max, w);\n    }\n    return max;\n  }, [m, chars]);\n\n  if (!m) {\n    return <div style={{ position: \"absolute\", inset: 0, background: c.bg }} />;\n  }\n\n  const F = Math.min((W * 0.84) / widest, H * 0.36);\n  const F2 = Math.min(F * 0.26, (W * 0.8) / Math.max(0.1, m.lineWidth));\n\n  const lineP = interpolate(t, [LINE_IN, LINE_IN + 20], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const lineSqueeze = interpolate(t, [LINE_IN, LINE_IN + 24], [0, 1], {\n    ...CLAMP,\n    easing: Easing.bezier(0.3, 0, 0.1, 1),\n  });\n  const lineAxes = mix({ ...NARROW, wdth: 100 }, NARROW, lineSqueeze);\n\n  const enter = interpolate(t, [0, 10], [0, 1], { ...CLAMP, easing: EASE_OUT });\n  const stops = c.fill(accent);\n  const angle = 100 + Math.sin(t / 45) * 6;\n  const fill = `linear-gradient(${angle.toFixed(2)}deg, ${stops[0]} 0%, ${stops[1]} 30%, ${stops[2]} 62%, ${stops[3]} 100%)`;\n\n  return (\n    <AbsoluteFill\n      style={{\n        background: c.bg,\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        flexDirection: \"column\",\n      }}\n    >\n      <div\n        style={{\n          display: \"flex\",\n          alignItems: \"baseline\",\n          justifyContent: \"center\",\n          whiteSpace: \"pre\",\n          fontFamily: flex.fontFamily,\n          fontSize: F,\n          lineHeight: 1,\n          // the fill box must cover the ink, descenders included; the extra\n          // bottom padding is pulled back with a negative margin\n          height: F * 1.08,\n          marginTop: F * 0.1,\n          marginBottom: -F * 0.22,\n          padding: `0 ${F * 0.04}px`,\n          backgroundImage: fill,\n          WebkitBackgroundClip: \"text\",\n          backgroundClip: \"text\",\n          color: \"transparent\",\n          opacity: 0.4 + 0.6 * enter,\n          transform: `translateY(${(1 - enter) * F * 0.08}px)`,\n        }}\n      >\n        {chars.map((ch, i) => {\n          const snap = spring({\n            frame: t - SETTLE - i * SETTLE_STAGGER,\n            fps,\n            config: SPRING_SNAP,\n          });\n          const l = level(i, t);\n          const axes = clampAxes(mix(mix(LOW, HIGH, l), FINAL, snap));\n          return (\n            <span\n              key={`${i}-${ch}`}\n              style={{\n                fontVariationSettings: fvs(axes),\n                letterSpacing: `${TRACK}em`,\n              }}\n            >\n              {ch}\n            </span>\n          );\n        })}\n      </div>\n      <div\n        style={{\n          overflow: \"hidden\",\n          marginTop: F * 0.2,\n          height: F2 * 1.2,\n        }}\n      >\n        <div\n          style={{\n            fontFamily: flex.fontFamily,\n            fontSize: F2,\n            lineHeight: 1.2,\n            whiteSpace: \"pre\",\n            color: accent,\n            fontVariationSettings: fvs(lineAxes),\n            letterSpacing: \"0.01em\",\n            transform: `translateY(${(1 - lineP) * F2 * 1.25}px)`,\n          }}\n        >\n          {line}\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/axis-flex.tsx"
    }
  ],
  "type": "registry:component"
}
