{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pull-in",
  "title": "Pull in",
  "description": "'Now in' stays put while spaced-out place words pull into view, such as vertical, 4K, your codebase.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/pull-in/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadJakarta } from \"@remotion/google-fonts/PlusJakartaSans\";\nimport type React from \"react\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n  AbsoluteFill,\n  continueRender,\n  delayRender,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst jakarta = loadJakarta(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\n\nexport interface PullInProps {\n  lead?: string;\n  places?: string[];\n  hold?: number;\n  accent?: string;\n  background?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst THEMES = {\n  dark: {\n    bg: \"#0B1F33\",\n    ink: \"#E8EEF2\",\n    muted: \"#7F93A6\",\n    teal: \"#00BFA5\",\n  },\n  light: {\n    bg: \"#E8EEF2\",\n    ink: \"#0B1F33\",\n    muted: \"#5C7083\",\n    teal: \"#00A08A\",\n  },\n} as const;\n\nconst WGHT = 500;\nconst LEAD_WGHT = 400;\nconst TRACK = -0.028;\nconst WORD = 0.14;\nconst FONT_PX = 200;\n\nconst HEADWAY = 1.25;\nconst BRAKE = 20;\nconst NOSE = 0.045;\nconst SETTLE = 12;\nconst BUNCH = 0.93;\nconst FOLLOW = 1.3;\nconst ARRIVE = BRAKE + SETTLE;\nconst DEPART_GAP = 9;\nconst LEAD_AT = 0;\nconst FIRST_AT = 4;\n\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\ninterface WordMetrics {\n  x: number[];\n  adv: number[];\n  width: number;\n}\n\ninterface Metrics {\n  places: WordMetrics[];\n  lead: number;\n  asc: number;\n  desc: number;\n  capTop: number;\n}\n\nconst spaces = (s: string) => s.split(\" \").length - 1;\n\nfunction measureWord(ctx: CanvasRenderingContext2D, s: string): WordMetrics {\n  const chars = Array.from(s);\n  const x: number[] = [];\n  let prefix = \"\";\n  for (let i = 0; i < chars.length; i++) {\n    x.push(\n      ctx.measureText(prefix).width / FONT_PX +\n        TRACK * i +\n        WORD * spaces(prefix),\n    );\n    prefix += chars[i];\n  }\n  const width =\n    ctx.measureText(s).width / FONT_PX +\n    TRACK * Math.max(0, chars.length - 1) +\n    WORD * spaces(s);\n  const adv = chars.map(\n    (_, i) => (i + 1 < chars.length ? x[i + 1] : width) - x[i],\n  );\n  return { x, adv, width };\n}\n\nfunction measure(places: string[], lead: string): Metrics {\n  const canvas = document.createElement(\"canvas\");\n  const ctx = canvas.getContext(\"2d\");\n  if (!ctx) throw new Error(\"pull-in: no 2d context\");\n  ctx.font = `${WGHT} ${FONT_PX}px \"${jakarta.fontFamily}\"`;\n  const m = places.map((p) => measureWord(ctx, p));\n  const fm = ctx.measureText(\"Hxg\");\n  const cap = ctx.measureText(\"H\");\n  ctx.font = `${LEAD_WGHT} ${FONT_PX}px \"${jakarta.fontFamily}\"`;\n  const leadW = lead\n    ? ctx.measureText(lead).width / FONT_PX + WORD * spaces(lead)\n    : 0;\n  return {\n    places: m,\n    lead: leadW,\n    asc: fm.fontBoundingBoxAscent / FONT_PX,\n    desc: fm.fontBoundingBoxDescent / FONT_PX,\n    capTop: cap.actualBoundingBoxAscent / FONT_PX,\n  };\n}\n\nfunction useMetrics(places: string[], lead: string) {\n  const key = [lead, ...places].join(\"\\u0001\");\n  const [state, setState] = useState<{ key: string; m: Metrics } | null>(null);\n  const [handle] = useState(() => delayRender(\"pull-in: measuring\"));\n  const released = useRef(false);\n  useEffect(() => {\n    let live = true;\n    const [l, ...p] = key.split(\"\\u0001\");\n    Promise.all([\n      jakarta.waitUntilDone(),\n      document.fonts.load(`${WGHT} 100px \"${jakarta.fontFamily}\"`),\n    ])\n      .catch(() => undefined)\n      .then(() => {\n        if (!live) return;\n        setState({ key, m: measure(p, l) });\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\nfunction arrival(tau: number, x0: number, nose: number) {\n  const v0 = (2 * x0) / BRAKE;\n  if (tau <= 0) return x0 - v0 * tau;\n  if (tau < BRAKE) {\n    const u = 1 - tau / BRAKE;\n    return x0 * u * u - nose * Math.sin((Math.PI / 2) * (1 - u)) ** 3;\n  }\n  const k = (tau - BRAKE) / SETTLE;\n  if (k >= 1) return 0;\n  return -nose * Math.cos((Math.PI / 2) * k) * (1 - k) ** 0.5;\n}\n\nfunction departure(tau: number) {\n  if (tau <= 0) return 0;\n  return 1.4 * tau * tau + 0.04 * tau * tau * tau;\n}\n\nexport function PullIn({\n  lead = \"Now in\",\n  places = [\"vertical.\", \"4K.\", \"your codebase.\"],\n  hold = 24,\n  accent,\n  background,\n  speed = 1,\n  theme = \"dark\",\n}: PullInProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const c = THEMES[theme];\n  const hi = accent ?? c.teal;\n  const list = useMemo(\n    () => (places.length > 0 ? places : [\"your codebase.\"]),\n    [places],\n  );\n  const m = useMetrics(list, lead);\n\n  const transparent = background === \"transparent\";\n  const stage = transparent ? \"transparent\" : (background ?? c.bg);\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.085);\n  const measureW = W - marginX * 2;\n  const widest = Math.max(...m.places.map((p) => p.width), 0.1);\n  const F = Math.min(H * (portrait ? 0.16 : 0.34), measureW / widest);\n  const L = lead\n    ? Math.min(F * 0.5, (measureW * 0.9) / Math.max(0.1, m.lead))\n    : 0;\n  const leadGap = lead ? F * 0.16 : 0;\n\n  const placeCap = m.capTop * F;\n  const leadCap = m.capTop * L;\n  const blockH = (lead ? leadCap + leadGap + F * 0.42 : 0) + placeCap;\n  const blockTop = H / 2 - blockH / 2 - F * 0.04;\n  const placeBase = blockTop + blockH;\n  const leadBase = blockTop + leadCap;\n\n  const STEP = ARRIVE + Math.max(8, hold) + DEPART_GAP;\n  const arriveAt = (k: number) => FIRST_AT + k * STEP;\n  const departAt = (k: number) => arriveAt(k + 1) - DEPART_GAP;\n  const x0 = W - marginX + F * 0.6;\n\n  interface Glyph {\n    k: number;\n    i: number;\n    ch: string;\n    x: number;\n  }\n  const glyphs: Glyph[] = [];\n  list.forEach((word, k) => {\n    const chars = Array.from(word);\n    const start = arriveAt(k);\n    const leave = k === list.length - 1 ? null : departAt(k);\n    if (t < start - chars.length * HEADWAY - 30) return;\n    if (leave !== null && t > leave + 40 + chars.length * HEADWAY) return;\n    chars.forEach((ch, i) => {\n      let dx = arrival(t - start - i * HEADWAY, x0, NOSE * F);\n      if (leave !== null) dx -= departure(t - leave - i * HEADWAY);\n      glyphs.push({ k, i, ch, x: marginX + m.places[k].x[i] * F + dx });\n    });\n  });\n  for (let g = 1; g < glyphs.length; g++) {\n    const prev = glyphs[g - 1];\n    const cur = glyphs[g];\n    const advPrev = m.places[prev.k].adv[prev.i] * F;\n    const floor =\n      prev.k === cur.k\n        ? prev.x + advPrev * BUNCH\n        : prev.x + advPrev + FOLLOW * F;\n    if (cur.x < floor) cur.x = floor;\n  }\n\n  const letters: React.ReactNode[] = [];\n  for (const { k, i, ch, x } of glyphs) {\n    const wm = m.places[k];\n    if (x > W + F || x + wm.adv[i] * F < -F) continue;\n    const last = k === list.length - 1;\n    letters.push(\n      <div\n        key={`p${k}-${i}`}\n        style={{\n          position: \"absolute\",\n          left: x,\n          top: placeBase - (m.asc + (1 - m.asc - m.desc) / 2) * F,\n          height: F,\n          lineHeight: `${F}px`,\n          whiteSpace: \"pre\",\n          fontFamily: jakarta.fontFamily,\n          fontWeight: WGHT,\n          fontSize: F,\n          color: last ? hi : c.ink,\n        }}\n      >\n        {ch}\n      </div>,\n    );\n  }\n\n  const leadP = interpolate(t, [LEAD_AT, LEAD_AT + 16], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  return (\n    <AbsoluteFill\n      style={{\n        background: stage,\n        overflow: \"hidden\",\n      }}\n    >\n      {lead ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: marginX,\n            top: leadBase - ((1.4 - m.asc - m.desc) / 2 + m.asc) * L,\n            height: L * 1.4,\n            overflow: \"hidden\",\n          }}\n        >\n          <div\n            style={{\n              fontFamily: jakarta.fontFamily,\n              fontWeight: LEAD_WGHT,\n              fontSize: L,\n              lineHeight: `${L * 1.4}px`,\n              height: L * 1.4,\n              letterSpacing: \"-0.02em\",\n              wordSpacing: `${WORD}em`,\n              whiteSpace: \"pre\",\n              color: c.muted,\n              transform: `translateY(${((1 - leadP) * L * 1.2).toFixed(2)}px)`,\n            }}\n          >\n            {lead}\n          </div>\n        </div>\n      ) : null}\n      {letters}\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/pull-in.tsx"
    }
  ],
  "type": "registry:component"
}
