{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "stitch-path",
  "type": "registry:ui",
  "title": "Stitch Path",
  "description": "A running stitch sewn along an SVG path as the page scrolls, following the holes it is laid over.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/loomui/stitch-path.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface StitchPathProps extends Omit<\n  React.ComponentProps<\"svg\">,\n  \"target\"\n> {\n  /** Path data, in the `0 0 100 100` box the component draws in. */\n  d?: string\n  /** Length of one stitch. */\n  stitch?: number\n  /** Bare thread between two stitches. */\n  gap?: number\n  /** Thread thickness in pixels. It does not stretch with the box. */\n  thickness?: number\n  /** Sew it yourself, 0 to 1. Leave it out to sew on scroll. */\n  progress?: number\n  /** Follow this element through the viewport instead of the path's own box. */\n  target?: React.RefObject<HTMLElement | null>\n  /** Draw the holes the thread is sewn through. */\n  guide?: boolean\n}\n\nconst DEFAULT_PATH = \"M 0 62 C 18 6 42 6 52 48 S 78 96 100 34\"\n\nconst clamp = (value: number) => Math.min(Math.max(value, 0), 1)\n\nexport function StitchPath({\n  d = DEFAULT_PATH,\n  stitch = 5,\n  gap = 4,\n  thickness = 2,\n  progress,\n  target,\n  guide = true,\n  className,\n  ...props\n}: StitchPathProps) {\n  const maskId = React.useId()\n  const hostRef = React.useRef<SVGSVGElement>(null)\n  const revealRef = React.useRef<SVGPathElement>(null)\n  const controlled = progress !== undefined\n\n  React.useEffect(() => {\n    const node = revealRef.current\n    if (!node) {\n      return\n    }\n\n    // `pathLength=\"1\"` normalises the path, so the offset is the fraction of it\n    // still to be sewn and the geometry never has to be measured.\n    const draw = (value: number) => {\n      node.style.strokeDashoffset = `${1 - clamp(value)}`\n    }\n\n    if (controlled) {\n      draw(progress)\n      return\n    }\n\n    if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) {\n      draw(1)\n      return\n    }\n\n    let frame = 0\n\n    const measure = () => {\n      const box = target?.current ?? hostRef.current\n      if (!box) {\n        return\n      }\n\n      // Zero when the top edge sits on the bottom of the viewport, one when the\n      // bottom edge has cleared the top of it.\n      const rect = box.getBoundingClientRect()\n      const travel = window.innerHeight + rect.height\n      draw(travel > 0 ? (window.innerHeight - rect.top) / travel : 1)\n    }\n\n    const onScroll = () => {\n      cancelAnimationFrame(frame)\n      frame = requestAnimationFrame(measure)\n    }\n\n    measure()\n    window.addEventListener(\"scroll\", onScroll, { passive: true })\n    window.addEventListener(\"resize\", onScroll, { passive: true })\n    return () => {\n      window.removeEventListener(\"scroll\", onScroll)\n      window.removeEventListener(\"resize\", onScroll)\n      cancelAnimationFrame(frame)\n    }\n  }, [controlled, progress, target])\n\n  return (\n    <svg\n      ref={hostRef}\n      data-slot=\"stitch-path\"\n      // Decoration. There is nothing here a reader would want announced.\n      aria-hidden=\"true\"\n      viewBox=\"0 0 100 100\"\n      preserveAspectRatio=\"none\"\n      fill=\"none\"\n      className={cn(\n        \"text-primary pointer-events-none h-full w-full\",\n        className\n      )}\n      {...props}\n    >\n      <defs>\n        {/* The reveal is the same path stroked solid and wiped by its own dash\n            offset. Wiping the visible path directly would cost it the dash\n            pattern that makes it read as stitches rather than a line. */}\n        <mask id={maskId}>\n          <path\n            ref={revealRef}\n            d={d}\n            pathLength=\"1\"\n            strokeDasharray=\"1 1\"\n            strokeDashoffset=\"1\"\n            stroke=\"white\"\n            strokeWidth=\"12\"\n            fill=\"none\"\n          />\n        </mask>\n      </defs>\n\n      {guide ? (\n        <path\n          d={d}\n          stroke=\"currentColor\"\n          strokeWidth={thickness}\n          strokeLinecap=\"round\"\n          // Zero-length dashes with a round cap are dots: one hole per stitch.\n          strokeDasharray={`0 ${stitch + gap}`}\n          vectorEffect=\"non-scaling-stroke\"\n          className=\"opacity-25\"\n        />\n      ) : null}\n\n      <path\n        d={d}\n        stroke=\"currentColor\"\n        strokeWidth={thickness}\n        strokeLinecap=\"round\"\n        strokeDasharray={`${stitch} ${gap}`}\n        vectorEffect=\"non-scaling-stroke\"\n        mask={`url(#${maskId})`}\n      />\n    </svg>\n  )\n}\n",
      "type": "registry:ui"
    }
  ]
}