{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ink-arrow",
  "title": "Ink Arrow",
  "description": "A hand-drawn brush arrow that sweeps itself from one point to another, thickening into the head that pops on the final poses.",
  "dependencies": ["remotion"],
  "registryDependencies": ["@remocn/stop-motion", "@remocn/brush"],
  "files": [
    {
      "path": "registry/remocn/ink-arrow/index.tsx",
      "content": "\"use client\";\n\nimport { useCurrentFrame } from \"remotion\";\nimport {\n  BrushGrain,\n  type BrushPoint,\n  brushFilterId,\n  brushReach,\n  brushRibbon,\n  sampleCubic,\n  sampleLine,\n} from \"@/components/remocn/brush\";\nimport { DEFAULT_STEP, hashRange, steppedRamp } from \"@/lib/remocn/stop-motion\";\n\nconst HEAD_ANGLE = 0.72;\nconst SHAFT_SAMPLES = 40;\nconst ARM_SAMPLES = 10;\nconst ARM_TAPER = 0.6;\nconst ARM_OVERSHOOT = 0.16;\nconst HEAD_MIN_RATIO = 2.6;\n\nexport type InkArrowPoint = BrushPoint;\n\nexport function inkArrowControls(\n  from: InkArrowPoint,\n  to: InkArrowPoint,\n  curvature: number,\n  seed: string,\n): { c1: InkArrowPoint; c2: InkArrowPoint } {\n  const dx = to.x - from.x;\n  const dy = to.y - from.y;\n  const distance = Math.hypot(dx, dy) || 1;\n  const normalX = -dy / distance;\n  const normalY = dx / distance;\n  const bow = curvature * distance;\n  const wobble = distance * 0.06;\n\n  const at = (t: number, key: string): InkArrowPoint => ({\n    x:\n      from.x +\n      dx * t +\n      normalX * bow +\n      hashRange(`${seed}:${key}:x`, -wobble, wobble),\n    y:\n      from.y +\n      dy * t +\n      normalY * bow +\n      hashRange(`${seed}:${key}:y`, -wobble, wobble),\n  });\n\n  return { c1: at(0.3, \"c1\"), c2: at(0.7, \"c2\") };\n}\n\nexport function inkArrowHead(\n  c2: InkArrowPoint,\n  to: InkArrowPoint,\n  headSize: number,\n): { left: InkArrowPoint; right: InkArrowPoint } {\n  const dx = to.x - c2.x;\n  const dy = to.y - c2.y;\n  const length = Math.hypot(dx, dy) || 1;\n  const backX = -dx / length;\n  const backY = -dy / length;\n\n  const arm = (angle: number): InkArrowPoint => ({\n    x: to.x + headSize * (backX * Math.cos(angle) - backY * Math.sin(angle)),\n    y: to.y + headSize * (backX * Math.sin(angle) + backY * Math.cos(angle)),\n  });\n\n  return { left: arm(HEAD_ANGLE), right: arm(-HEAD_ANGLE) };\n}\n\nexport function inkArrowArmWidth(strokeWidth: number, release: number): number {\n  return strokeWidth * release;\n}\n\nexport function inkArrowApex(\n  c2: InkArrowPoint,\n  to: InkArrowPoint,\n  reach: number,\n): InkArrowPoint {\n  const dx = to.x - c2.x;\n  const dy = to.y - c2.y;\n  const length = Math.hypot(dx, dy) || 1;\n  return {\n    x: to.x + (dx / length) * reach * ARM_OVERSHOOT,\n    y: to.y + (dy / length) * reach * ARM_OVERSHOOT,\n  };\n}\n\nexport function inkArrowHeadReach(\n  headSize: number,\n  strokeWidth: number,\n): number {\n  return Math.max(headSize, strokeWidth * HEAD_MIN_RATIO);\n}\n\nexport function inkArrowViewport(\n  from: InkArrowPoint,\n  to: InkArrowPoint,\n  curvature: number,\n  headSize: number,\n  strokeWidth: number,\n  grain = 1,\n): { width: number; height: number } {\n  const distance = Math.hypot(to.x - from.x, to.y - from.y);\n  const margin =\n    inkArrowHeadReach(headSize, strokeWidth) +\n    Math.abs(curvature) * distance +\n    brushReach(strokeWidth, grain);\n  return {\n    width: Math.max(margin, Math.max(from.x, to.x) + margin),\n    height: Math.max(margin, Math.max(from.y, to.y) + margin),\n  };\n}\n\nexport interface InkArrowProps {\n  from: InkArrowPoint;\n  to: InkArrowPoint;\n  curvature?: number;\n  color?: string;\n  strokeWidth?: number;\n  pressure?: number;\n  release?: number;\n  grain?: number;\n  delay?: number;\n  drawDur?: number;\n  headSize?: number;\n  headDur?: number;\n  seed?: string;\n  step?: number;\n}\n\nexport function InkArrow({\n  from,\n  to,\n  curvature = 0.35,\n  color = \"#26242c\",\n  strokeWidth = 8,\n  pressure = 0.2,\n  release = 1,\n  grain = 1,\n  delay = 0,\n  drawDur = 36,\n  headSize = 24,\n  headDur,\n  seed = \"arrow\",\n  step = DEFAULT_STEP,\n}: InkArrowProps) {\n  const frame = useCurrentFrame();\n  const progress = steppedRamp(frame, delay, delay + drawDur, { step });\n  const { c1, c2 } = inkArrowControls(from, to, curvature, seed);\n  const headReach = inkArrowHeadReach(headSize, strokeWidth);\n  const head = inkArrowHead(c2, to, headReach);\n  const apex = inkArrowApex(c2, to, headReach);\n\n  const headFrames = headDur ?? step * 4;\n  const headStart = delay + drawDur;\n  const headMid = headStart + headFrames / 2;\n  const leftArm = steppedRamp(frame, headStart, headMid, { step });\n  const rightArm = steppedRamp(frame, headMid, headStart + headFrames, {\n    step,\n  });\n\n  const { width, height } = inkArrowViewport(\n    from,\n    to,\n    curvature,\n    headSize,\n    strokeWidth,\n    grain,\n  );\n\n  const filterId = brushFilterId(seed, strokeWidth, grain);\n  const filter = grain > 0 ? `url(#${filterId})` : undefined;\n\n  const armRibbon = (tip: InkArrowPoint, armProgress: number) =>\n    brushRibbon(sampleLine(apex, tip, ARM_SAMPLES), {\n      strokeWidth: inkArrowArmWidth(strokeWidth, release),\n      pressure: 1,\n      release: ARM_TAPER,\n      progress: armProgress,\n    });\n\n  return (\n    <svg\n      width={width}\n      height={height}\n      viewBox={`0 0 ${width} ${height}`}\n      style={{\n        display: \"block\",\n        overflow: \"visible\",\n        opacity: progress > 0 ? 1 : 0,\n      }}\n    >\n      <title>Ink arrow</title>\n      <BrushGrain seed={seed} strokeWidth={strokeWidth} grain={grain} />\n      <path\n        d={brushRibbon(sampleCubic(from, c1, c2, to, SHAFT_SAMPLES), {\n          strokeWidth,\n          pressure,\n          release,\n          progress,\n        })}\n        fill={color}\n        opacity={0.85}\n        filter={filter}\n      />\n      {leftArm > 0 ? (\n        <path\n          d={armRibbon(head.left, leftArm)}\n          fill={color}\n          opacity={0.85}\n          filter={filter}\n        />\n      ) : null}\n      {rightArm > 0 ? (\n        <path\n          d={armRibbon(head.right, rightArm)}\n          fill={color}\n          opacity={0.85}\n          filter={filter}\n        />\n      ) : null}\n    </svg>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/ink-arrow.tsx"
    }
  ],
  "type": "registry:component"
}
