{
  "name": "path-draw",
  "type": "registry:ui",
  "description": "SVG stroke reveal with auto-fit framing, multi-path stagger and a drawing head",
  "dependencies": [
    "remotion",
    "@remotion/paths"
  ],
  "registryDependencies": [
    "path-utils",
    "motion-tokens"
  ],
  "files": [
    {
      "path": "registry/bases/default/primitives/path-draw.tsx",
      "type": "registry:ui",
      "content": "import { interpolate, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { EASING } from \"@/remotion/lib/motion-tokens\";\nimport {\n  clampProgress,\n  fitViewBox,\n  getPathDrawStyles,\n  samplePath,\n} from \"@/remotion/lib/path-utils\";\n\nexport type PathDrawProps = {\n  /** One path, or several drawn in order with `staggerInFrames` between them. */\n  d: string | string[];\n  durationInFrames?: number;\n  delayInFrames?: number;\n  /** Offset between the start of each path when `d` is an array. */\n  staggerInFrames?: number;\n  stroke?: string;\n  strokeWidth?: number;\n  width?: number;\n  height?: number;\n  /** Omit to frame the artwork automatically from its bounding box. */\n  viewBox?: string;\n  /** Colour flooded in behind the stroke once a path finishes drawing. */\n  fill?: string;\n  /** Bright dot riding the tip of the stroke while it draws. */\n  head?: boolean;\n  glow?: boolean;\n};\n\n/** Padding in path units so the stroke is not clipped by an auto viewBox. */\nconst AUTO_FIT_PADDING = 8;\n\nexport const PathDraw: React.FC<PathDrawProps> = ({\n  d,\n  durationInFrames = 60,\n  delayInFrames = 0,\n  staggerInFrames = 8,\n  stroke = \"#e8b86d\",\n  strokeWidth = 4,\n  width = 200,\n  height = 200,\n  viewBox,\n  fill,\n  head = true,\n  glow = true,\n}) => {\n  const frame = useCurrentFrame();\n  const { id } = useVideoConfig();\n  const paths = (Array.isArray(d) ? d : [d]).filter(Boolean);\n  // Filter ids are document-global, so two PathDraws mounted on one frame\n  // would share a filter. Scoping by composition keeps them independent.\n  const filterId = `path-draw-glow-${id}`;\n  const resolvedViewBox = viewBox ?? fitViewBox(paths, AUTO_FIT_PADDING);\n  // A filter region defaults to a share of the element's bounding box, and a\n  // straight horizontal or vertical path has a zero-area box — which would\n  // drop the stroke entirely. Anchoring the region to the viewBox keeps every\n  // path filtered, degenerate or not.\n  const [boxX, boxY, boxWidth, boxHeight] = resolvedViewBox\n    .split(/[\\s,]+/)\n    .map(Number);\n\n  return (\n    <svg width={width} height={height} viewBox={resolvedViewBox}>\n      {glow ? (\n        <defs>\n          <filter\n            id={filterId}\n            filterUnits=\"userSpaceOnUse\"\n            x={boxX}\n            y={boxY}\n            width={boxWidth}\n            height={boxHeight}\n          >\n            <feGaussianBlur stdDeviation=\"1.5\" result=\"blur\" />\n            <feMerge>\n              <feMergeNode in=\"blur\" />\n              <feMergeNode in=\"SourceGraphic\" />\n            </feMerge>\n          </filter>\n        </defs>\n      ) : null}\n\n      {paths.map((path, index) => {\n        const start = delayInFrames + index * staggerInFrames;\n        const progress = clampProgress(\n          interpolate(frame, [start, start + durationInFrames], [0, 1], {\n            extrapolateLeft: \"clamp\",\n            extrapolateRight: \"clamp\",\n            easing: EASING.enter,\n          }),\n        );\n        const { strokeDasharray, strokeDashoffset } = getPathDrawStyles(\n          progress,\n          path,\n        );\n        // A fill only reads once the outline closes, so it trails the stroke\n        // instead of competing with it.\n        const fillOpacity = fill\n          ? interpolate(\n              frame,\n              [start + durationInFrames * 0.75, start + durationInFrames * 1.3],\n              [0, 1],\n              {\n                extrapolateLeft: \"clamp\",\n                extrapolateRight: \"clamp\",\n                easing: EASING.enter,\n              },\n            )\n          : 0;\n        const tip =\n          head && progress > 0 && progress < 1\n            ? samplePath(path, progress)\n            : null;\n\n        return (\n          <g key={`${path.slice(0, 16)}-${index}`}>\n            {fill ? (\n              <path d={path} fill={fill} opacity={fillOpacity} stroke=\"none\" />\n            ) : null}\n            <path\n              d={path}\n              fill=\"none\"\n              stroke={stroke}\n              strokeWidth={strokeWidth}\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              strokeDasharray={strokeDasharray}\n              strokeDashoffset={strokeDashoffset}\n              filter={glow ? `url(#${filterId})` : undefined}\n              opacity={0.95}\n            />\n            {tip ? (\n              <circle\n                cx={tip.x}\n                cy={tip.y}\n                r={strokeWidth * 0.9}\n                fill={stroke}\n                filter={glow ? `url(#${filterId})` : undefined}\n              />\n            ) : null}\n          </g>\n        );\n      })}\n    </svg>\n  );\n};\n"
    }
  ],
  "atlas": {
    "lane": "vectors",
    "drive": "time",
    "tier": "advanced"
  }
}