{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pendulum-wave",
  "title": "Pendulum Wave",
  "description": "A row of pendulums with tuned periods drifts from a straight line into waves and braids, then realigns for a seamless loop.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/pendulum-wave/index.tsx",
      "content": "\"use client\";\n\nimport { AbsoluteFill, useCurrentFrame, useVideoConfig } from \"remotion\";\n\nexport type PendulumWaveProps = {\n  /** Number of pendulums. */\n  count?: number;\n  /** Swings the slowest pendulum makes per loop; pendulum i makes cycles + i. */\n  cycles?: number;\n  /** Frames until every pendulum realigns. */\n  loopFrames?: number;\n  speed?: number;\n  background?: string;\n  /** Bobs and guides. */\n  color?: string;\n  /** The wave line threaded through the bobs. */\n  accent?: string;\n  /** Draw the faint per-pendulum tracks and centre line. */\n  showGuides?: boolean;\n};\n\nexport const pendulumWaveDefaults: Required<PendulumWaveProps> = {\n  count: 15,\n  cycles: 10,\n  loopFrames: 450,\n  speed: 1,\n  background: \"#111111\",\n  color: \"#FFFFFF\",\n  accent: \"#E5322D\",\n  showGuides: true,\n};\n\nconst TAU = Math.PI * 2;\nconst SMEAR_FRAMES = 4;\nconst SMEAR_SAMPLES = 6;\n\ntype Point = { x: number; y: number };\n\nconst fmt = (n: number) => Math.round(n * 10) / 10;\n\n// Catmull-Rom through the bobs so the wave reads as one continuous curve.\nfunction smoothPath(points: Point[]) {\n  if (points.length < 2) return \"\";\n  let d = `M ${fmt(points[0].x)} ${fmt(points[0].y)}`;\n  for (let i = 0; i < points.length - 1; i++) {\n    const p0 = points[i - 1] ?? points[i];\n    const p1 = points[i];\n    const p2 = points[i + 1];\n    const p3 = points[i + 2] ?? p2;\n    const c1x = p1.x + (p2.x - p0.x) / 6;\n    const c1y = p1.y + (p2.y - p0.y) / 6;\n    const c2x = p2.x - (p3.x - p1.x) / 6;\n    const c2y = p2.y - (p3.y - p1.y) / 6;\n    d += ` C ${fmt(c1x)} ${fmt(c1y)} ${fmt(c2x)} ${fmt(c2y)} ${fmt(p2.x)} ${fmt(p2.y)}`;\n  }\n  return d;\n}\n\nconst circle = (x: number, y: number, r: number) =>\n  `M ${fmt(x - r)} ${fmt(y)} a ${fmt(r)} ${fmt(r)} 0 1 0 ${fmt(r * 2)} 0 a ${fmt(r)} ${fmt(r)} 0 1 0 ${fmt(-r * 2)} 0`;\n\nexport function PendulumWave({\n  count = pendulumWaveDefaults.count,\n  cycles = pendulumWaveDefaults.cycles,\n  loopFrames = pendulumWaveDefaults.loopFrames,\n  speed = pendulumWaveDefaults.speed,\n  background = pendulumWaveDefaults.background,\n  color = pendulumWaveDefaults.color,\n  accent = pendulumWaveDefaults.accent,\n  showGuides = pendulumWaveDefaults.showGuides,\n}: PendulumWaveProps) {\n  const frame = useCurrentFrame();\n  const { width, height } = useVideoConfig();\n  const n = Math.max(2, Math.round(count));\n  const loop = Math.max(1, loopFrames);\n  const time = frame * speed;\n\n  const marginX = width * 0.1;\n  const gap = (width - marginX * 2) / (n - 1);\n  const amp = height * 0.3;\n  const cy = height / 2;\n  const r0 = Math.min(gap * 0.3, height * 0.022);\n  const unit = Math.min(width, height) / 720;\n\n  // Phase of pendulum i: 2π · t · (cycles + i) / T. At t = 0 and t = T all\n  // pendulums sit at the top extreme, so the loop closes on a straight line.\n  const phaseAt = (i: number, t: number) => (TAU * t * (cycles + i)) / loop;\n\n  const bobs = Array.from({ length: n }, (_, i) => {\n    const c = Math.cos(phaseAt(i, time));\n    // Fake depth: bobs swinging toward the viewer (lower) read slightly bigger.\n    return { x: marginX + i * gap, y: cy - amp * c, r: r0 * (1 - 0.22 * c) };\n  });\n\n  let smear = \"\";\n  for (let i = 0; i < n; i++) {\n    const x = fmt(bobs[i].x);\n    for (let s = 0; s <= SMEAR_SAMPLES; s++) {\n      const t = time - (SMEAR_FRAMES * speed * s) / SMEAR_SAMPLES;\n      const y = fmt(cy - amp * Math.cos(phaseAt(i, t)));\n      smear += `${s === 0 ? \"M\" : \"L\"} ${x} ${y} `;\n    }\n  }\n\n  const tracks = bobs\n    .map((b) => `M ${fmt(b.x)} ${fmt(cy - amp)} V ${fmt(cy + amp)}`)\n    .join(\" \");\n  const ticks = bobs\n    .map(\n      (b) =>\n        circle(b.x, cy - amp, 2.2 * unit) + circle(b.x, cy + amp, 2.2 * unit),\n    )\n    .join(\" \");\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=\"Pendulum wave\"\n        style={{ display: \"block\" }}\n      >\n        {showGuides ? (\n          <g stroke={color} fill=\"none\">\n            <path\n              d={`M ${fmt(marginX - gap)} ${cy} H ${fmt(width - marginX + gap)}`}\n              strokeOpacity={0.14}\n              strokeWidth={unit}\n            />\n            <path\n              d={tracks}\n              strokeOpacity={0.08}\n              strokeWidth={unit}\n              strokeDasharray={`${2 * unit} ${6 * unit}`}\n            />\n            <path d={ticks} fill={color} fillOpacity={0.25} stroke=\"none\" />\n          </g>\n        ) : null}\n        <path\n          d={smoothPath(bobs)}\n          fill=\"none\"\n          stroke={accent}\n          strokeWidth={2.5 * unit}\n          strokeLinecap=\"round\"\n          strokeOpacity={0.85}\n        />\n        <path\n          d={smear}\n          fill=\"none\"\n          stroke={color}\n          strokeOpacity={0.13}\n          strokeWidth={r0 * 0.9}\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n        <path\n          d={bobs.map((b) => circle(b.x, b.y, b.r)).join(\" \")}\n          fill={color}\n        />\n      </svg>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/pendulum-wave.tsx"
    }
  ],
  "type": "registry:component"
}
