{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "brush",
  "title": "Brush",
  "description": "Variable-width brush ribbons for Remotion. Pressure taper, grain displacement, and spine samplers shared by the ink marks.",
  "registryDependencies": ["@remocn/stop-motion"],
  "files": [
    {
      "path": "registry/remocn/brush/index.tsx",
      "content": "\"use client\";\n\nimport { hash01 } from \"@/lib/remocn/stop-motion\";\n\nconst GRAIN_RATIO = 0.5;\nconst GRAIN_FREQUENCY = 0.7;\nconst GRAIN_OCTAVES = 3;\n\nexport type BrushPoint = { x: number; y: number };\n\nexport type BrushTaper = {\n  strokeWidth: number;\n  pressure?: number;\n  release?: number;\n};\n\nexport function brushHalfWidth(taper: BrushTaper, t: number): number {\n  const pressure = taper.pressure ?? 0.2;\n  const release = taper.release ?? 1;\n  return (taper.strokeWidth / 2) * (pressure + (release - pressure) * t);\n}\n\nexport function brushGrainScale(strokeWidth: number, grain: number): number {\n  return strokeWidth * GRAIN_RATIO * grain;\n}\n\nexport function brushReach(strokeWidth: number, grain: number): number {\n  return strokeWidth / 2 + brushGrainScale(strokeWidth, grain) / 2;\n}\n\nexport function brushFilterId(\n  seed: string,\n  strokeWidth: number,\n  grain: number,\n): string {\n  return `remocn-brush-${Math.floor(hash01(`${seed}:${strokeWidth}:${grain}`) * 1e9)}`;\n}\n\nexport function sampleCubic(\n  from: BrushPoint,\n  c1: BrushPoint,\n  c2: BrushPoint,\n  to: BrushPoint,\n  points = 32,\n): BrushPoint[] {\n  const count = Math.max(2, points);\n  return Array.from({ length: count }, (_, i) => {\n    const t = i / (count - 1);\n    const u = 1 - t;\n    const a = u * u * u;\n    const b = 3 * u * u * t;\n    const c = 3 * u * t * t;\n    const d = t * t * t;\n    return {\n      x: a * from.x + b * c1.x + c * c2.x + d * to.x,\n      y: a * from.y + b * c1.y + c * c2.y + d * to.y,\n    };\n  });\n}\n\nexport function sampleLine(\n  from: BrushPoint,\n  to: BrushPoint,\n  points = 8,\n): BrushPoint[] {\n  const count = Math.max(2, points);\n  return Array.from({ length: count }, (_, i) => {\n    const t = i / (count - 1);\n    return {\n      x: from.x + (to.x - from.x) * t,\n      y: from.y + (to.y - from.y) * t,\n    };\n  });\n}\n\nconst normalAt = (points: BrushPoint[], i: number): BrushPoint => {\n  const before = points[i - 1] ?? points[i];\n  const after = points[i + 1] ?? points[i];\n  const tx = after.x - before.x;\n  const ty = after.y - before.y;\n  const length = Math.hypot(tx, ty) || 1;\n  return { x: -ty / length, y: tx / length };\n};\n\nconst curveSegments = (points: BrushPoint[]): string =>\n  points\n    .slice(0, -1)\n    .map((p1, i) => {\n      const p0 = points[i - 1] ?? p1;\n      const p2 = points[i + 1];\n      const p3 = points[i + 2] ?? p2;\n      return `C ${p1.x + (p2.x - p0.x) / 6} ${p1.y + (p2.y - p0.y) / 6}, ${p2.x - (p3.x - p1.x) / 6} ${p2.y - (p3.y - p1.y) / 6}, ${p2.x} ${p2.y}`;\n    })\n    .join(\" \");\n\nexport function brushRibbon(\n  spine: BrushPoint[],\n  options: BrushTaper & { progress?: number },\n): string {\n  const progress = options.progress ?? 1;\n  const drawn = Math.max(2, Math.round(progress * (spine.length - 1)) + 1);\n\n  const left: BrushPoint[] = [];\n  const right: BrushPoint[] = [];\n  for (let i = 0; i < drawn; i++) {\n    const half = brushHalfWidth(options, i / (spine.length - 1));\n    const normal = normalAt(spine, i);\n    left.push({\n      x: spine[i].x + normal.x * half,\n      y: spine[i].y + normal.y * half,\n    });\n    right.push({\n      x: spine[i].x - normal.x * half,\n      y: spine[i].y - normal.y * half,\n    });\n  }\n  const back = right.slice().reverse();\n\n  return `M ${left[0].x} ${left[0].y} ${curveSegments(left)} L ${back[0].x} ${back[0].y} ${curveSegments(back)} Z`;\n}\n\nexport function BrushGrain({\n  seed,\n  strokeWidth,\n  grain,\n}: {\n  seed: string;\n  strokeWidth: number;\n  grain: number;\n}) {\n  return (\n    <defs>\n      <filter\n        id={brushFilterId(seed, strokeWidth, grain)}\n        x=\"-30%\"\n        y=\"-30%\"\n        width=\"160%\"\n        height=\"160%\"\n      >\n        <feTurbulence\n          type=\"fractalNoise\"\n          baseFrequency={GRAIN_FREQUENCY}\n          numOctaves={GRAIN_OCTAVES}\n          seed={Math.floor(hash01(`${seed}:grain`) * 1000)}\n          result=\"brushGrain\"\n        />\n        <feDisplacementMap\n          in=\"SourceGraphic\"\n          in2=\"brushGrain\"\n          scale={brushGrainScale(strokeWidth, grain)}\n          xChannelSelector=\"R\"\n          yChannelSelector=\"G\"\n        />\n      </filter>\n    </defs>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/brush.tsx"
    }
  ],
  "type": "registry:component"
}
