{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "layover",
  "title": "Layover",
  "description": "A headline's key word cycles through a short list of stops before settling on the final word.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/layover/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/RedHatDisplay\";\nimport type React from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport {\n  AbsoluteFill,\n  continueRender,\n  delayRender,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst redHat = loadFont(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\n\nexport interface LayoverProps {\n  from?: string;\n  word?: string;\n  stops?: string[];\n  to?: string;\n  accent?: string;\n  background?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst THEMES = {\n  dark: { bg: \"#B84A26\", ink: \"#FFF1E0\", accent: \"#8FD3FF\" },\n  light: { bg: \"#FFF1E0\", ink: \"#B84A26\", accent: \"#1B4F72\" },\n} as const;\n\nconst WGHT = 500;\nconst TRACK = -0.03;\nconst FONT_PX = 200;\n\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst EASE_LEG = Easing.bezier(0.65, 0, 0.35, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nconst A_IN = 0;\nconst WORD_STAGGER = 3;\nconst LIFT = 26;\nconst LEG = 22;\nconst BOARD = 11;\nconst HOLD = 20;\nconst SETTLE = 10;\n\ninterface Metrics {\n  w: Record<string, number>;\n  asc: number;\n  desc: number;\n}\n\nfunction measureAll(texts: string[]): Metrics {\n  const canvas = document.createElement(\"canvas\");\n  const ctx = canvas.getContext(\"2d\");\n  if (!ctx) throw new Error(\"layover: no 2d context\");\n  ctx.font = `${WGHT} ${FONT_PX}px \"${redHat.fontFamily}\"`;\n  const w: Record<string, number> = {};\n  for (const s of texts) {\n    w[s] = ctx.measureText(s).width / FONT_PX + TRACK * Array.from(s).length;\n  }\n  const fm = ctx.measureText(\"Hxg\");\n  return {\n    w,\n    asc: fm.fontBoundingBoxAscent / FONT_PX,\n    desc: fm.fontBoundingBoxDescent / FONT_PX,\n  };\n}\n\nfunction useMetrics(texts: string[]) {\n  const key = texts.join(\"\\u0001\");\n  const [state, setState] = useState<{ key: string; m: Metrics } | null>(null);\n  const [handle] = useState(() => delayRender(\"layover: measuring\"));\n  const released = useRef(false);\n  useEffect(() => {\n    let live = true;\n    Promise.all([\n      redHat.waitUntilDone(),\n      document.fonts.load(`${WGHT} 100px \"${redHat.fontFamily}\"`),\n    ])\n      .catch(() => undefined)\n      .then(() => {\n        if (!live) return;\n        setState({ key, m: measureAll(key.split(\"\\u0001\")) });\n        if (!released.current) {\n          released.current = true;\n          continueRender(handle);\n        }\n      });\n    return () => {\n      live = false;\n    };\n  }, [key, handle]);\n  return state && state.key === key ? state.m : null;\n}\n\nexport function Layover({\n  from = \"Write it\",\n  word = \"here.\",\n  stops = [\"w\", \"every\"],\n  to = \"Plays\",\n  accent,\n  background,\n  speed = 1,\n  theme = \"dark\",\n}: LayoverProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const c = THEMES[theme];\n  const hi = accent ?? c.accent;\n  const stage =\n    background === \"transparent\" ? \"transparent\" : (background ?? c.bg);\n\n  const finalWord = [...stops].reverse().join(\"\") + word;\n  const fromWords = from.split(\" \").filter(Boolean);\n  const lineA = `${from} ${word}`;\n  const lineB = `${to} ${finalWord}`;\n  const m = useMetrics([\n    lineA,\n    lineB,\n    word,\n    finalWord,\n    ...stops,\n    ...fromWords.map((_, i) => fromWords.slice(0, i).join(\" \")),\n    ...fromWords.map((_, i) => `${fromWords.slice(0, i + 1).join(\" \")} `),\n  ]);\n\n  if (!m) {\n    return (\n      <div style={{ position: \"absolute\", inset: 0, background: stage }} />\n    );\n  }\n\n  const portrait = H > W * 1.1;\n  const marginX = W * (portrait ? 0.08 : 0.07);\n  const marginY = H * (portrait ? 0.1 : 0.11);\n  const measureW = W - marginX * 2;\n  const widest = Math.max(m.w[lineA], m.w[lineB], 0.1);\n  const F = Math.min(H * (portrait ? 0.075 : 0.19), measureW / widest);\n\n  const boxTop = (base: number) =>\n    base - ((1 - m.asc - m.desc) / 2 + m.asc) * F;\n  const capH = F * 0.7;\n  const baseA = marginY + capH;\n  const baseB = H - marginY;\n\n  const leftA = marginX;\n  const rx0 = leftA + m.w[lineA] * F;\n  const rx3 = W - marginX;\n  const leftB = rx3 - m.w[lineB] * F;\n\n  const n = stops.length;\n  const legStart = (k: number) => LIFT + k * (LEG + HOLD);\n  const arriveAt = (k: number) => legStart(k) + LEG;\n  const landAt = arriveAt(n);\n  const fracs = Array.from({ length: n + 2 }, (_, k) => k / (n + 1));\n\n  let progress = 0;\n  for (let k = 0; k <= n; k++) {\n    const p = interpolate(t, [legStart(k), arriveAt(k)], [0, 1], {\n      ...CLAMP,\n      easing: EASE_LEG,\n    });\n    progress = p > 0 ? fracs[k] + p * (fracs[k + 1] - fracs[k]) : progress;\n  }\n  const rx = rx0 + (rx3 - rx0) * progress;\n  const base = baseA + (baseB - baseA) * progress;\n  const lift =\n    interpolate(t, [LIFT - 6, LIFT + 4], [0, 1], CLAMP) *\n    (1 - interpolate(t, [landAt - 8, landAt], [0, 1], CLAMP));\n  const touch =\n    t > landAt\n      ? Math.sin(Math.min(1, (t - landAt) / SETTLE) * Math.PI) *\n        F *\n        0.025 *\n        (1 - Math.min(1, (t - landAt) / SETTLE))\n      : 0;\n\n  const textStyle = (size: number): React.CSSProperties => ({\n    position: \"absolute\",\n    height: size,\n    lineHeight: `${size}px`,\n    whiteSpace: \"pre\",\n    fontFamily: redHat.fontFamily,\n    fontWeight: WGHT,\n    fontSize: size,\n    letterSpacing: `${TRACK}em`,\n  });\n\n  const rise = (start: number) =>\n    interpolate(t, [start, start + 16], [0, 1], { ...CLAMP, easing: EASE_OUT });\n\n  const nodes: React.ReactNode[] = [];\n\n  const wordsA = [...fromWords, word];\n  wordsA.forEach((wd, i) => {\n    const x =\n      i === 0\n        ? 0\n        : i < fromWords.length\n          ? m.w[`${fromWords.slice(0, i).join(\" \")} `] * F\n          : m.w[`${fromWords.join(\" \")} `] * F;\n    const p = rise(A_IN + i * WORD_STAGGER);\n    nodes.push(\n      <div\n        key={`a${i}`}\n        style={{\n          position: \"absolute\",\n          left: leftA + x,\n          top: boxTop(baseA) - F * 0.1,\n          height: F * 1.3,\n          overflow: \"hidden\",\n        }}\n      >\n        <div\n          style={{\n            ...textStyle(F),\n            position: \"relative\",\n            top: F * 0.1,\n            color: c.ink,\n            transform: `translateY(${((1 - p) * F * 1.4).toFixed(2)}px)`,\n          }}\n        >\n          {wd}\n        </div>\n      </div>,\n    );\n  });\n\n  const toIn = rise(legStart(n) - 4);\n  nodes.push(\n    <div\n      key=\"to\"\n      style={{\n        position: \"absolute\",\n        left: leftB,\n        top: boxTop(baseB) - F * 0.1,\n        height: F * 1.3,\n        overflow: \"hidden\",\n      }}\n    >\n      <div\n        style={{\n          ...textStyle(F),\n          position: \"relative\",\n          top: F * 0.1,\n          color: c.ink,\n          transform: `translateY(${((1 - toIn) * F * 1.4).toFixed(2)}px)`,\n        }}\n      >\n        {to}\n      </div>\n    </div>,\n  );\n\n  if (t >= LIFT - 6) {\n    let xRight = rx - m.w[word] * F;\n    const parts: { s: string; x: number; p: number }[] = [\n      { s: word, x: xRight, p: 1 },\n    ];\n    stops.forEach((s, k) => {\n      xRight -= m.w[s] * F;\n      const p = interpolate(\n        t,\n        [arriveAt(k) + 2, arriveAt(k) + 2 + BOARD],\n        [0, 1],\n        { ...CLAMP, easing: EASE_OUT },\n      );\n      parts.push({ s, x: xRight, p });\n    });\n    for (let i = parts.length - 1; i >= 0; i--) {\n      const part = parts[i];\n      if (part.p <= 0) continue;\n      nodes.push(\n        <div\n          key={`w${i}`}\n          style={{\n            ...textStyle(F),\n            left: part.x,\n            top: boxTop(base) - lift * F * 0.04 + touch,\n            color: hi,\n            opacity: part.p,\n            transform: `translateY(${(-(1 - part.p) * F * 0.55).toFixed(2)}px)`,\n          }}\n        >\n          {part.s}\n        </div>,\n      );\n    }\n  }\n\n  return (\n    <AbsoluteFill style={{ background: stage, overflow: \"hidden\" }}>\n      {nodes}\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/layover.tsx"
    }
  ],
  "type": "registry:component"
}
