{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "string-art",
  "title": "String Art",
  "description": "Threads string a ring of pins times-table style while the multiplier morphs from cardioid to nephroid and beyond.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/string-art/index.tsx",
      "content": "\"use client\";\n\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nexport type StringArtProps = {\n  /** Pins around the ring. */\n  pins?: number;\n  /** Multiplier at the start (2 draws a cardioid). */\n  from?: number;\n  /** Multiplier at the end (3 is a nephroid, 4 a three-cusp epicycloid). */\n  to?: number;\n  /** Frames for the threads to be strung, one pin after another. */\n  stringFrames?: number;\n  /** Frames for the multiplier to drift from `from` to `to`. */\n  morphFrames?: number;\n  /** Length of the accent sweep, as a share of all threads; 0 disables. */\n  sweep?: number;\n  /** Pins per frame the accent sweep travels once the threads are strung. */\n  sweepSpeed?: number;\n  /** Thread opacity, 0 to 1. */\n  threadOpacity?: number;\n  /** Show the running multiplier in the corner. */\n  showLabel?: boolean;\n  speed?: number;\n  background?: string;\n  color?: string;\n  accent?: string;\n};\n\nexport const stringArtDefaults: Required<StringArtProps> = {\n  pins: 200,\n  from: 2,\n  to: 4,\n  stringFrames: 60,\n  morphFrames: 240,\n  sweep: 0.08,\n  sweepSpeed: 0.9,\n  threadOpacity: 0.42,\n  showLabel: true,\n  speed: 1,\n  background: \"#111111\",\n  color: \"#FFFFFF\",\n  accent: \"#E5322D\",\n};\n\nconst TAU = Math.PI * 2;\nconst MORPH_DELAY = 24;\n// Share of each integer step spent holding on the clean integer figure.\nconst HOLD = 0.3;\nconst SWEEP_BUCKETS = 4;\nconst fmt = (n: number) => Math.round(n * 10) / 10;\n\nexport function StringArt({\n  pins = stringArtDefaults.pins,\n  from = stringArtDefaults.from,\n  to = stringArtDefaults.to,\n  stringFrames: stringFramesProp = stringArtDefaults.stringFrames,\n  morphFrames: morphFramesProp = stringArtDefaults.morphFrames,\n  sweep = stringArtDefaults.sweep,\n  sweepSpeed = stringArtDefaults.sweepSpeed,\n  threadOpacity = stringArtDefaults.threadOpacity,\n  showLabel = stringArtDefaults.showLabel,\n  speed = stringArtDefaults.speed,\n  background = stringArtDefaults.background,\n  color = stringArtDefaults.color,\n  accent = stringArtDefaults.accent,\n}: StringArtProps) {\n  const frame = useCurrentFrame();\n  const { width, height } = useVideoConfig();\n  const n = Math.max(8, Math.round(pins));\n  const unit = Math.min(width, height) / 720;\n  const radius = Math.min(width, height) * 0.42;\n  const cx = width / 2;\n  const cy = height / 2;\n  const time = frame * speed;\n  // interpolate() needs a strictly increasing range.\n  const stringFrames = Math.max(1, stringFramesProp);\n  const morphFrames = Math.max(1, morphFramesProp);\n\n  const strung = interpolate(time, [0, stringFrames], [0, n], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: Easing.inOut(Easing.quad),\n  });\n  const morphStart = stringFrames + MORPH_DELAY;\n  // The multiplier walks through each integer table (cardioid, nephroid, ...)\n  // with an eased step and a short hold on every clean figure.\n  const span = to - from;\n  const steps = Math.max(1, Math.ceil(Math.abs(span) - 1e-6));\n  const u =\n    interpolate(time, [morphStart, morphStart + morphFrames], [0, steps], {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n    }) - 1e-9;\n  const step = Math.max(0, Math.floor(u));\n  const within = Easing.inOut(Easing.cubic)(\n    Math.min(1, Math.max(0, (u - step) / (1 - HOLD))),\n  );\n  const k = from + (span * Math.min(steps, step + within)) / steps;\n\n  // Pin 0 sits on the left so the cardioid cusp points left and the figure is\n  // symmetric about the horizontal axis. Angles stay continuous in k, so the\n  // pattern morphs smoothly instead of snapping between integer tables.\n  const point = (index: number) => {\n    const a = Math.PI + (TAU * index) / n;\n    return { x: cx + Math.cos(a) * radius, y: cy + Math.sin(a) * radius };\n  };\n  const xy = (p: { x: number; y: number }) => `${fmt(p.x)} ${fmt(p.y)}`;\n\n  let threads = \"\";\n  const accents: string[] = new Array(SWEEP_BUCKETS).fill(\"\");\n  const whole = Math.floor(strung);\n  // The accent sweep rides the stringing head, then keeps circling the ring.\n  const head = strung + Math.max(0, time - stringFrames) * sweepSpeed;\n  const tail = Math.max(0, sweep) * n;\n  for (let i = 0; i < Math.ceil(strung); i++) {\n    const start = point(i);\n    const end = point(k * i);\n    // The thread being strung right now grows from its pin to the target.\n    const f = i < whole ? 1 : strung - whole;\n    const tip = {\n      x: start.x + (end.x - start.x) * f,\n      y: start.y + (end.y - start.y) * f,\n    };\n    const d = `M ${xy(start)} L ${xy(tip)} `;\n    const behind = (((head - i) % n) + n) % n;\n    if (tail > 0 && behind < tail) {\n      accents[\n        Math.min(SWEEP_BUCKETS - 1, Math.floor((behind / tail) * SWEEP_BUCKETS))\n      ] += d;\n    } else threads += d;\n  }\n\n  let pinDots = \"\";\n  const pr = 1.6 * unit;\n  for (let i = 0; i < n; i++) {\n    const { x, y } = point(i);\n    pinDots += `M ${fmt(x - pr)} ${fmt(y)} a ${fmt(pr)} ${fmt(pr)} 0 1 0 ${fmt(pr * 2)} 0 a ${fmt(pr)} ${fmt(pr)} 0 1 0 ${fmt(-pr * 2)} 0 `;\n  }\n  const pinsIn = interpolate(time, [0, 12], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n  const labelIn = interpolate(\n    time,\n    [stringFrames - 10, stringFrames + 6],\n    [0, 1],\n    {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n    },\n  );\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: background }}>\n      <svg\n        width=\"100%\"\n        height=\"100%\"\n        viewBox={`0 0 ${width} ${height}`}\n        role=\"img\"\n        aria-label=\"String art times table\"\n        style={{ display: \"block\" }}\n      >\n        <path d={pinDots} fill={color} fillOpacity={0.55 * pinsIn} />\n        <path\n          d={threads}\n          fill=\"none\"\n          stroke={color}\n          strokeOpacity={threadOpacity}\n          strokeWidth={unit}\n          strokeLinecap=\"round\"\n        />\n        <g fill=\"none\" stroke={accent} strokeLinecap=\"round\">\n          {accents.map((d, b) => (\n            <path\n              key={b}\n              d={d}\n              strokeOpacity={1 - b / SWEEP_BUCKETS}\n              strokeWidth={unit * (1.6 - b * 0.15)}\n            />\n          ))}\n        </g>\n      </svg>\n      {showLabel ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: width * 0.06,\n            bottom: height * 0.08,\n            fontFamily: \"ui-monospace, SFMono-Regular, Menlo, monospace\",\n            fontSize: 22 * unit,\n            letterSpacing: \"0.04em\",\n            color,\n            opacity: 0.7 * labelIn,\n            fontVariantNumeric: \"tabular-nums\",\n          }}\n        >\n          <span style={{ color: accent }}>i → </span>\n          {k.toFixed(2)} · i mod {n}\n        </div>\n      ) : null}\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/string-art.tsx"
    }
  ],
  "type": "registry:component"
}
