{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"moving-lines","type":"registry:ui","title":"Moving Lines","description":"Fine diagonal lines drift sideways while bright comets of light race along them. Canvas 2D.","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/ui/moving-lines.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nexport interface MovingLinesProps {\n  /** Base line color. */\n  color?: string;\n  /** Color of the traveling highlights. */\n  highlightColor?: string;\n  /** Px between lines. */\n  gap?: number;\n  /** Line angle in degrees. */\n  angle?: number;\n  /** Drift speed multiplier; 0 freezes the lines. */\n  speed?: number;\n  /** Highlights launched per second. */\n  highlights?: number;\n  className?: string;\n}\n\nfunction rgb(hex: string) {\n  let h = hex.replace(\"#\", \"\");\n  if (h.length === 3) h = h.replace(/./g, \"$&$&\");\n  const n = parseInt(h.padEnd(6, \"0\").slice(0, 6), 16) || 0;\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\n\n// Stable per-line brightness so lines differ without flickering.\nconst lineHash = (m: number) => {\n  const s = Math.sin(m * 127.1) * 43758.5453;\n  return s - Math.floor(s);\n};\n\ninterface Highlight {\n  m: number;\n  y: number;\n  v: number;\n  len: number;\n}\n\nexport function MovingLines({\n  color = \"#71717a\",\n  highlightColor = \"#67e8f9\",\n  gap = 26,\n  angle = 35,\n  speed = 1,\n  highlights = 2.5,\n  className,\n}: MovingLinesProps) {\n  const ref = useRef<HTMLCanvasElement>(null);\n  const opts = useRef({ c: rgb(color), hl: rgb(highlightColor), gap, angle, speed, highlights });\n  const redraw = useRef<() => void>(undefined);\n\n  useEffect(() => {\n    opts.current = { c: rgb(color), hl: rgb(highlightColor), gap, angle, speed, highlights };\n    redraw.current?.();\n  }, [color, highlightColor, gap, angle, speed, highlights]);\n\n  useEffect(() => {\n    const canvas = ref.current!;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    const lights: Highlight[] = [];\n    let w = 0;\n    let h = 0;\n    let offset = 0;\n    let carry = 0;\n    let raf = 0;\n    let last = 0;\n\n    const draw = (dt: number) => {\n      const o = opts.current;\n      const g = Math.max(4, o.gap);\n      const half = Math.hypot(w, h) / 2;\n      offset += dt * o.speed * 14;\n\n      // Launch and advance highlights in the rotated frame (lines run along y).\n      carry += dt * o.speed * o.highlights;\n      while (carry >= 1 || (dt === 0 && lights.length < Math.round(o.highlights * 1.5))) {\n        if (carry >= 1) carry -= 1;\n        const m = Math.floor((Math.random() * 2 - 1) * (half / g) - offset / g);\n        const len = 90 + Math.random() * 200;\n        const v = (220 + Math.random() * 380) * (Math.random() < 0.5 ? -1 : 1);\n        const y = dt === 0 ? (Math.random() * 2 - 1) * half : v > 0 ? -half - len : half + len;\n        lights.push({ m, y, v, len });\n      }\n      for (let i = lights.length - 1; i >= 0; i--) {\n        const l = lights[i];\n        l.y += l.v * dt * o.speed;\n        const x = l.m * g + offset;\n        if (Math.abs(l.y) > half + l.len * 1.2 || Math.abs(x) > half + g) lights.splice(i, 1);\n      }\n\n      ctx.setTransform(1, 0, 0, 1, 0, 0);\n      ctx.clearRect(0, 0, canvas.width, canvas.height);\n      const dpr = canvas.width / Math.max(w, 1);\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      ctx.translate(w / 2, h / 2);\n      ctx.rotate((o.angle * Math.PI) / 180);\n\n      // Base lines, bucketed into three brightness levels.\n      const [r, gg, b] = o.c;\n      const m0 = Math.floor((-half - offset) / g);\n      const m1 = Math.ceil((half - offset) / g);\n      ctx.lineWidth = 1;\n      for (let bucket = 0; bucket < 3; bucket++) {\n        ctx.beginPath();\n        for (let m = m0; m <= m1; m++) {\n          if (Math.floor(lineHash(m) * 3) !== bucket) continue;\n          const x = m * g + offset;\n          ctx.moveTo(x, -half);\n          ctx.lineTo(x, half);\n        }\n        ctx.strokeStyle = `rgba(${r},${gg},${b},${0.14 + bucket * 0.1})`;\n        ctx.stroke();\n      }\n\n      // Highlights: a bright head with a long fading tail plus a soft halo.\n      const [hr, hg, hb] = o.hl;\n      ctx.lineCap = \"round\";\n      for (const l of lights) {\n        const x = l.m * g + offset;\n        const dir = Math.sign(l.v);\n        const tail = l.y - dir * l.len;\n        const grad = ctx.createLinearGradient(x, tail, x, l.y);\n        grad.addColorStop(0, `rgba(${hr},${hg},${hb},0)`);\n        grad.addColorStop(0.85, `rgba(${hr},${hg},${hb},0.85)`);\n        grad.addColorStop(1, `rgba(255,255,255,1)`);\n        ctx.strokeStyle = grad;\n        ctx.globalAlpha = 0.25;\n        ctx.lineWidth = 5;\n        ctx.beginPath();\n        ctx.moveTo(x, tail);\n        ctx.lineTo(x, l.y);\n        ctx.stroke();\n        ctx.globalAlpha = 1;\n        ctx.lineWidth = 1.4;\n        ctx.stroke();\n      }\n    };\n\n    const loop = (now: number) => {\n      draw(last ? Math.min((now - last) / 1000, 0.1) : 0);\n      last = now;\n      raf = requestAnimationFrame(loop);\n    };\n\n    const resize = () => {\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      w = canvas.offsetWidth;\n      h = canvas.offsetHeight;\n      canvas.width = Math.max(1, Math.round(w * dpr));\n      canvas.height = Math.max(1, Math.round(h * dpr));\n      draw(0);\n    };\n\n    const ro = new ResizeObserver(resize);\n    ro.observe(canvas);\n    const io = new IntersectionObserver(([e]) => {\n      cancelAnimationFrame(raf);\n      raf = 0;\n      last = 0;\n      if (e.isIntersecting && !reduced) raf = requestAnimationFrame(loop);\n    });\n    io.observe(canvas);\n    redraw.current = () => {\n      if (!raf) draw(0);\n    };\n\n    return () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      io.disconnect();\n      redraw.current = undefined;\n    };\n  }, []);\n\n  const mask = \"radial-gradient(ellipse 75% 75% at 50% 50%, #000 30%, transparent 100%)\";\n  return (\n    <canvas\n      ref={ref}\n      aria-hidden\n      className={className}\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        width: \"100%\",\n        height: \"100%\",\n        pointerEvents: \"none\",\n        maskImage: mask,\n        WebkitMaskImage: mask,\n      }}\n    />\n  );\n}\n"}]}