{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"column-lines","type":"registry:ui","title":"Column Lines","description":"Evenly spaced column lines with colored light pulses falling down them at random. Canvas 2D.","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/ui/column-lines.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nexport interface ColumnLinesProps {\n  /** Pulse colors, picked at random per pulse. */\n  colors?: string[];\n  /** Column line color. */\n  lineColor?: string;\n  /** Distance between columns in px. */\n  gap?: number;\n  /** Fall speed multiplier; 0 freezes the pulses. */\n  speed?: number;\n  /** New pulses per second, per 10 columns. */\n  frequency?: number;\n  className?: string;\n}\n\ninterface Pulse {\n  col: number;\n  y: number;\n  v: number;\n  len: number;\n  color: string;\n}\n\nconst DEFAULT_COLORS = [\"#a78bfa\", \"#f472b6\", \"#38bdf8\"];\n\nexport function ColumnLines({\n  colors = DEFAULT_COLORS,\n  lineColor = \"#2f2f35\",\n  gap = 40,\n  speed = 1,\n  frequency = 2,\n  className,\n}: ColumnLinesProps) {\n  const host = useRef<HTMLDivElement>(null);\n  const opts = useRef({ colors, lineColor, gap, speed, frequency });\n  const redraw = useRef<(() => void) | null>(null);\n\n  useEffect(() => {\n    opts.current = { colors, lineColor, gap, speed, frequency };\n    redraw.current?.();\n  }, [colors, lineColor, gap, speed, frequency]);\n\n  useEffect(() => {\n    const el = host.current!;\n    const canvas = el.querySelector(\"canvas\")!;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n    let dpr = 1;\n    let raf = 0;\n    let visible = true;\n    let last = 0;\n    let pulses: Pulse[] = [];\n    let budget = 0;\n\n    const geometry = () => {\n      const g = Math.max(8, opts.current.gap) * dpr;\n      const cols = Math.max(1, Math.floor(canvas.width / g));\n      const ox = (canvas.width - (cols - 1) * g) / 2;\n      return { g, cols, ox };\n    };\n\n    const spawn = (y?: number): Pulse => {\n      const { cols } = geometry();\n      const H = canvas.height;\n      const palette = opts.current.colors.length ? opts.current.colors : DEFAULT_COLORS;\n      const len = H * (0.08 + Math.random() * 0.22);\n      return {\n        col: (Math.random() * cols) | 0,\n        y: y ?? -Math.random() * H * 0.1,\n        v: H * (0.25 + Math.random() * 0.45),\n        len,\n        color: palette[(Math.random() * palette.length) | 0],\n      };\n    };\n\n    const draw = () => {\n      const o = opts.current;\n      const W = canvas.width;\n      const H = canvas.height;\n      const { g, cols, ox } = geometry();\n      ctx.clearRect(0, 0, W, H);\n\n      // Columns fade out toward the top and bottom edges.\n      const fade = ctx.createLinearGradient(0, 0, 0, H);\n      fade.addColorStop(0, \"transparent\");\n      fade.addColorStop(0.2, o.lineColor);\n      fade.addColorStop(0.8, o.lineColor);\n      fade.addColorStop(1, \"transparent\");\n      ctx.fillStyle = fade;\n      for (let c = 0; c < cols; c++) ctx.fillRect(Math.round(ox + c * g), 0, dpr, H);\n\n      ctx.globalCompositeOperation = \"lighter\";\n      for (const p of pulses) {\n        const x = Math.round(ox + p.col * g) + dpr / 2;\n        const head = p.y;\n        const tail = p.y - p.len;\n        // Edge fade so pulses do not pop in or out.\n        const edge = Math.min(1, Math.max(0, head / (H * 0.12)), Math.max(0, (H - tail) / (H * 0.25)));\n        ctx.globalAlpha = edge;\n        const trail = ctx.createLinearGradient(0, tail, 0, head);\n        trail.addColorStop(0, \"transparent\");\n        trail.addColorStop(1, p.color);\n        ctx.fillStyle = trail;\n        ctx.fillRect(x - 1.5 * dpr, tail, 3 * dpr, p.len);\n        ctx.globalAlpha = edge * 0.2;\n        ctx.fillRect(x - 8 * dpr, tail + p.len * 0.4, 16 * dpr, p.len * 0.6);\n        ctx.globalAlpha = edge;\n        const r = 10 * dpr;\n        const glow = ctx.createRadialGradient(x, head, 0, x, head, r);\n        glow.addColorStop(0, \"#ffffff\");\n        glow.addColorStop(0.25, p.color);\n        glow.addColorStop(1, \"transparent\");\n        ctx.fillStyle = glow;\n        ctx.fillRect(x - r, head - r, r * 2, r * 2);\n      }\n      ctx.globalAlpha = 1;\n      ctx.globalCompositeOperation = \"source-over\";\n    };\n\n    const seed = () => {\n      const { cols } = geometry();\n      pulses = Array.from({ length: Math.round(cols * 0.15 * opts.current.frequency) + 1 }, () =>\n        spawn(Math.random() * canvas.height * 1.2),\n      );\n    };\n\n    const loop = (now: number) => {\n      const dt = (Math.min(now - last, 50) / 1000) * opts.current.speed;\n      last = now;\n      for (const p of pulses) p.y += p.v * dt;\n      pulses = pulses.filter((p) => p.y - p.len < canvas.height);\n      // Random arrivals at the requested rate.\n      budget += (geometry().cols / 10) * opts.current.frequency * dt;\n      while (budget >= 1) {\n        budget--;\n        if (Math.random() < 0.8) pulses.push(spawn());\n      }\n      draw();\n      raf = requestAnimationFrame(loop);\n    };\n    const play = () => {\n      if (raf || !visible || reduced) return;\n      last = performance.now();\n      raf = requestAnimationFrame(loop);\n    };\n    const resize = () => {\n      dpr = Math.min(window.devicePixelRatio || 1, 2);\n      canvas.width = Math.max(1, Math.round(el.clientWidth * dpr));\n      canvas.height = Math.max(1, Math.round(el.clientHeight * dpr));\n      seed();\n      draw();\n    };\n\n    const ro = new ResizeObserver(resize);\n    ro.observe(el);\n    const io = new IntersectionObserver(([e]) => {\n      visible = e.isIntersecting;\n      if (visible) play();\n      else {\n        cancelAnimationFrame(raf);\n        raf = 0;\n      }\n    });\n    io.observe(el);\n    redraw.current = draw;\n\n    return () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      io.disconnect();\n      redraw.current = null;\n    };\n  }, []);\n\n  return (\n    <div\n      ref={host}\n      aria-hidden\n      className={className}\n      style={{ position: \"absolute\", inset: 0, overflow: \"hidden\", pointerEvents: \"none\" }}\n    >\n      <canvas style={{ position: \"absolute\", inset: 0, width: \"100%\", height: \"100%\", display: \"block\" }} />\n    </div>\n  );\n}\n"}]}