{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "kinetic-warp",
  "title": "Kinetic Warp",
  "description": "A coarse mesh warp bends a whole word in staccato beats, one axis per keyframe.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/kinetic-warp/index.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n  continueRender,\n  delayRender,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n} from \"remotion\";\n\nexport interface KineticWarpProps {\n  className?: string;\n  compWidth?: number;\n  compHeight?: number;\n  textColor?: string;\n  text?: string;\n  fontFamily?: string;\n  fontUrl?: string;\n  fontWeight?: number;\n  fontSize?: number;\n  leading?: number;\n  tracking?: number;\n  layerScale?: number;\n  positionX?: number;\n  positionY?: number;\n  meshRows?: number;\n  meshColumns?: number;\n  colRestX?: number;\n  colRightX?: number;\n  colLeftX?: number;\n  rowRestY?: number;\n  rowDownY?: number;\n  rowUpY?: number;\n  keyframeStride?: number;\n  easingBezier?: [number, number, number, number];\n  resampleStep?: number;\n}\n\nfunction monotoneSpline(xs: number[], ys: number[]): (x: number) => number {\n  const n = xs.length;\n  const dx: number[] = [];\n  const secant: number[] = [];\n  for (let i = 0; i < n - 1; i++) {\n    dx.push(xs[i + 1] - xs[i]);\n    secant.push((ys[i + 1] - ys[i]) / (xs[i + 1] - xs[i]));\n  }\n\n  const m: number[] = new Array(n);\n  m[0] = secant[0];\n  m[n - 1] = secant[n - 2];\n  for (let i = 1; i < n - 1; i++) {\n    m[i] = secant[i - 1] * secant[i] <= 0 ? 0 : (secant[i - 1] + secant[i]) / 2;\n  }\n\n  for (let i = 0; i < n - 1; i++) {\n    if (secant[i] === 0) {\n      m[i] = 0;\n      m[i + 1] = 0;\n      continue;\n    }\n    const a = m[i] / secant[i];\n    const b = m[i + 1] / secant[i];\n    const s = a * a + b * b;\n    if (s > 9) {\n      const t = 3 / Math.sqrt(s);\n      m[i] = t * a * secant[i];\n      m[i + 1] = t * b * secant[i];\n    }\n  }\n\n  return (x: number) => {\n    if (x <= xs[0]) return ys[0] + m[0] * (x - xs[0]);\n    if (x >= xs[n - 1]) return ys[n - 1] + m[n - 1] * (x - xs[n - 1]);\n    let i = n - 2;\n    while (i > 0 && x < xs[i]) i--;\n    const h = dx[i];\n    const t = (x - xs[i]) / h;\n    const t2 = t * t;\n    const t3 = t2 * t;\n    return (\n      (2 * t3 - 3 * t2 + 1) * ys[i] +\n      (t3 - 2 * t2 + t) * h * m[i] +\n      (-2 * t3 + 3 * t2) * ys[i + 1] +\n      (t3 - t2) * h * m[i + 1]\n    );\n  };\n}\n\nexport function KineticWarp({\n  className,\n  compWidth = 1920,\n  compHeight = 1080,\n  textColor = \"#FFFFFF\",\n  text = \"REM\\nOCN\",\n  fontFamily = \"sans-serif\",\n  fontUrl = \"\",\n  fontWeight = 700,\n  fontSize = 107.1,\n  leading = 70,\n  tracking = -21,\n  layerScale = 480,\n  positionX = 960,\n  positionY = 540,\n  meshRows = 2,\n  meshColumns = 4,\n  colRestX = 960,\n  colRightX = 1255,\n  colLeftX = 678,\n  rowRestY = 540,\n  rowDownY = 850,\n  rowUpY = 387,\n  keyframeStride = 20,\n  easingBezier = [0.3, 0.74, 0.09, 1],\n  resampleStep = 4,\n}: KineticWarpProps) {\n  const frame = useCurrentFrame();\n  const canvasRef = useRef<HTMLCanvasElement | null>(null);\n  const [fontReady, setFontReady] = useState(false);\n\n  const scale = layerScale / 100;\n  const pxSize = fontSize * scale;\n  const pxLeading = leading * scale;\n  const pxTracking = (tracking / 1000) * pxSize;\n\n  useEffect(() => {\n    const handle = delayRender(\"kinetic-warp: loading font\");\n    let cancelled = false;\n    const done = () => {\n      if (!cancelled) setFontReady(true);\n      continueRender(handle);\n    };\n    if (typeof document === \"undefined\" || !document.fonts) {\n      done();\n      return;\n    }\n    document.fonts\n      .load(`${fontWeight} ${pxSize}px \"${fontFamily}\"`)\n      .then(done, done);\n    return () => {\n      cancelled = true;\n    };\n  }, [fontFamily, fontWeight, pxSize]);\n\n  const source = useMemo(() => {\n    if (!fontReady || typeof document === \"undefined\") return null;\n    const canvas = document.createElement(\"canvas\");\n    canvas.width = compWidth;\n    canvas.height = compHeight;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return null;\n\n    ctx.font = `${fontWeight} ${pxSize}px \"${fontFamily}\", sans-serif`;\n    ctx.letterSpacing = `${pxTracking}px`;\n    ctx.fillStyle = textColor;\n    ctx.textAlign = \"left\";\n    ctx.textBaseline = \"alphabetic\";\n\n    const lines = text.split(\"\\n\");\n    const metrics = lines.map((line) => ctx.measureText(line));\n\n    const ascent = metrics[0].actualBoundingBoxAscent;\n    const descent = metrics[metrics.length - 1].actualBoundingBoxDescent;\n    const span = (lines.length - 1) * pxLeading;\n    const firstBaseline = positionY - (span - ascent + descent) / 2;\n\n    lines.forEach((line, i) => {\n      const m = metrics[i];\n      const x =\n        positionX - (m.actualBoundingBoxRight - m.actualBoundingBoxLeft) / 2;\n      ctx.fillText(line, x, firstBaseline + i * pxLeading);\n    });\n\n    return canvas;\n  }, [\n    compWidth,\n    compHeight,\n    text,\n    fontFamily,\n    fontWeight,\n    pxSize,\n    pxLeading,\n    pxTracking,\n    textColor,\n    positionX,\n    positionY,\n    fontReady,\n  ]);\n\n  const scratch = useMemo(() => {\n    if (typeof document === \"undefined\") return null;\n    const canvas = document.createElement(\"canvas\");\n    canvas.width = compWidth;\n    canvas.height = compHeight;\n    return canvas;\n  }, [compWidth, compHeight]);\n\n  const easing = Easing.bezier(...easingBezier);\n  const stops = [0, 1, 2, 3, 4, 5].map((i) => i * keyframeStride);\n  const clamp = {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing,\n  } as const;\n\n  const colX = interpolate(\n    frame,\n    stops,\n    [colRestX, colRightX, colRightX, colLeftX, colLeftX, colRestX],\n    clamp,\n  );\n  const rowY = interpolate(\n    frame,\n    stops,\n    [rowRestY, rowRestY, rowDownY, rowDownY, rowUpY, rowRestY],\n    clamp,\n  );\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas || !source || !scratch) return;\n    const out = canvas.getContext(\"2d\");\n    const mid = scratch.getContext(\"2d\");\n    if (!out || !mid) return;\n\n    const xs = Array.from(\n      { length: meshColumns + 1 },\n      (_, i) => (i * compWidth) / meshColumns,\n    );\n    const ys = Array.from(\n      { length: meshRows + 1 },\n      (_, i) => (i * compHeight) / meshRows,\n    );\n    const xd = xs.slice();\n    const yd = ys.slice();\n    xd[Math.round(meshColumns / 2)] = colX;\n    yd[Math.round(meshRows / 2)] = rowY;\n\n    const gx = monotoneSpline(xd, xs);\n    const gy = monotoneSpline(yd, ys);\n\n    mid.clearRect(0, 0, compWidth, compHeight);\n    out.clearRect(0, 0, compWidth, compHeight);\n\n    for (let d0 = 0; d0 < compWidth; d0 += resampleStep) {\n      const d1 = Math.min(d0 + resampleStep, compWidth);\n      const s0 = gx(d0);\n      const sw = gx(d1) - s0;\n      if (sw <= 0) continue;\n      mid.drawImage(source, s0, 0, sw, compHeight, d0, 0, d1 - d0, compHeight);\n    }\n\n    for (let d0 = 0; d0 < compHeight; d0 += resampleStep) {\n      const d1 = Math.min(d0 + resampleStep, compHeight);\n      const s0 = gy(d0);\n      const sh = gy(d1) - s0;\n      if (sh <= 0) continue;\n      out.drawImage(scratch, 0, s0, compWidth, sh, 0, d0, compWidth, d1 - d0);\n    }\n  }, [\n    source,\n    scratch,\n    colX,\n    rowY,\n    compWidth,\n    compHeight,\n    meshRows,\n    meshColumns,\n    resampleStep,\n  ]);\n\n  return (\n    <div\n      className={className}\n      style={{ position: \"absolute\", inset: 0, overflow: \"hidden\" }}\n    >\n      {fontUrl ? <link rel=\"stylesheet\" href={fontUrl} /> : null}\n      <canvas\n        ref={canvasRef}\n        width={compWidth}\n        height={compHeight}\n        style={{ width: \"100%\", height: \"100%\", display: \"block\" }}\n      />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/kinetic-warp.tsx"
    }
  ],
  "type": "registry:component"
}
