{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "beams-background",
  "title": "Beams Background",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "Hairline rails with thin light beams rising along them and dissolving near the top, over a soft glow. Canvas, token colors, DPR aware, pauses off-screen, a still frame under reduced motion.",
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/color.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/beams-background.tsx",
      "content": "// Ballmac UI: Beams Background. https://ui.ballmac.com/components/beams-background\n\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { cssColorToRgba, observeTheme } from \"@/lib/ballmac/color\"\n\ntype BeamsBackgroundProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** Distance between the hairline rails in pixels. Beams travel along the rails. */\n  gap?: number\n  /** Beams in flight per 1000 px of width. */\n  density?: number\n  /** Beam colors: CSS variable names (\"--chart-1\") or any CSS colors. Each beam picks one. */\n  colors?: string[]\n  /** Which way the beams travel. */\n  direction?: \"up\" | \"down\"\n  /** Speed multiplier. 1 is a calm rise. */\n  speed?: number\n  /** Opacity of the resting hairline rails (0–1). */\n  railOpacity?: number\n  /** Add a soft glow where the beams are born. */\n  glow?: boolean\n}\n\ntype Beam = { col: number; y: number; length: number; speed: number; color: number; width: number; life: number }\n\nfunction BeamsBackground({\n  gap = 36,\n  density = 30,\n  colors = [\"--chart-1\", \"--chart-2\", \"--chart-4\"],\n  direction = \"up\",\n  speed = 1,\n  railOpacity = 0.07,\n  glow = true,\n  className,\n  ...props\n}: BeamsBackgroundProps) {\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  const canvasRef = React.useRef<HTMLCanvasElement>(null)\n  const colorsKey = colors.join(\"|\")\n\n  React.useEffect(() => {\n    const root = rootRef.current\n    const canvas = canvasRef.current\n    const ctx = canvas?.getContext(\"2d\")\n    if (!root || !canvas || !ctx) return\n    const reduceMotion = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n    const palette = colorsKey.split(\"|\")\n\n    let width = 0\n    let height = 0\n    let cols = 0\n    let offset = 0\n    let rgba: [number, number, number][] = []\n    let rail = \"transparent\"\n    let beams: Beam[] = []\n\n    const readColors = () => {\n      rgba = palette.map((c) => cssColorToRgba(root, c).slice(0, 3).map((v) => Math.round(v * 255)) as [number, number, number])\n      const [r, g, b] = cssColorToRgba(root, \"--foreground\")\n      rail = `rgb(${Math.round(r * 255)} ${Math.round(g * 255)} ${Math.round(b * 255)} / ${railOpacity})`\n    }\n\n    const spawn = (anywhere: boolean): Beam => {\n      const length = 80 + Math.random() * Math.min(280, height * 0.7)\n      return {\n        col: Math.floor(Math.random() * cols),\n        // New beams start just past the edge they enter from; the first batch is scattered.\n        y: anywhere ? Math.random() * (height + length) : height + length,\n        length,\n        speed: (0.5 + Math.random() * 1.4) * speed,\n        color: Math.floor(Math.random() * rgba.length),\n        width: Math.random() < 0.2 ? 1.5 : 1,\n        life: 0.55 + Math.random() * 0.45,\n      }\n    }\n\n    const resize = () => {\n      const dpr = Math.min(window.devicePixelRatio || 1, 2)\n      width = root.clientWidth\n      height = root.clientHeight\n      canvas.width = Math.round(width * dpr)\n      canvas.height = Math.round(height * dpr)\n      canvas.style.width = `${width}px`\n      canvas.style.height = `${height}px`\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0)\n      cols = Math.max(1, Math.floor(width / gap))\n      // Center the rails so both edges get the same margin.\n      offset = (width - (cols - 1) * gap) / 2\n      const count = Math.max(3, Math.round((density * width) / 1000))\n      beams = Array.from({ length: count }, () => spawn(true))\n    }\n\n    const drawBeam = (beam: Beam) => {\n      const x = Math.round(offset + beam.col * gap) + 0.5\n      // Travel is computed bottom-up; flip for downward beams.\n      const head = direction === \"up\" ? beam.y - beam.length : height - beam.y + beam.length\n      const tail = direction === \"up\" ? beam.y : height - beam.y\n      const [r, g, b] = rgba[beam.color] ?? [255, 255, 255]\n      // Fade out as the beam nears the far edge, so beams dissolve instead of exiting.\n      const progress = 1 - (direction === \"up\" ? head : height - head) / height\n      const fade = Math.max(0, Math.min(1, (1 - progress) / 0.3)) * beam.life\n      if (fade <= 0) return\n      const gradient = ctx.createLinearGradient(x, tail, x, head)\n      gradient.addColorStop(0, `rgb(${r} ${g} ${b} / 0)`)\n      gradient.addColorStop(0.75, `rgb(${r} ${g} ${b} / ${0.55 * fade})`)\n      gradient.addColorStop(1, `rgb(${r} ${g} ${b} / ${fade})`)\n      ctx.strokeStyle = gradient\n      ctx.lineWidth = beam.width\n      ctx.beginPath()\n      ctx.moveTo(x, tail)\n      ctx.lineTo(x, head)\n      ctx.stroke()\n      // A small soft head, like light gathering at the tip.\n      const halo = ctx.createRadialGradient(x, head, 0, x, head, 6)\n      halo.addColorStop(0, `rgb(${r} ${g} ${b} / ${0.9 * fade})`)\n      halo.addColorStop(1, `rgb(${r} ${g} ${b} / 0)`)\n      ctx.fillStyle = halo\n      ctx.fillRect(x - 6, head - 6, 12, 12)\n    }\n\n    const draw = () => {\n      ctx.clearRect(0, 0, width, height)\n      ctx.strokeStyle = rail\n      ctx.lineWidth = 1\n      ctx.beginPath()\n      for (let c = 0; c < cols; c++) {\n        const x = Math.round(offset + c * gap) + 0.5\n        ctx.moveTo(x, 0)\n        ctx.lineTo(x, height)\n      }\n      ctx.stroke()\n      for (const beam of beams) drawBeam(beam)\n    }\n\n    const step = () => {\n      for (let i = 0; i < beams.length; i++) {\n        const beam = beams[i]!\n        beam.y -= beam.speed\n        if (beam.y < height * 0.1) beams[i] = spawn(false)\n      }\n    }\n\n    let frame = 0\n    let visible = true\n    const loop = () => {\n      step()\n      draw()\n      frame = requestAnimationFrame(loop)\n    }\n    const sync = () => {\n      cancelAnimationFrame(frame)\n      if (reduceMotion) draw()\n      else if (visible && !document.hidden) frame = requestAnimationFrame(loop)\n    }\n\n    readColors()\n    resize()\n    sync()\n    const ro = new ResizeObserver(() => {\n      resize()\n      if (reduceMotion) draw()\n    })\n    ro.observe(root)\n    const io = new IntersectionObserver(([entry]) => {\n      visible = entry?.isIntersecting ?? true\n      sync()\n    })\n    io.observe(root)\n    const stopTheme = observeTheme(() => {\n      readColors()\n      if (reduceMotion) draw()\n    })\n    document.addEventListener(\"visibilitychange\", sync)\n    return () => {\n      cancelAnimationFrame(frame)\n      ro.disconnect()\n      io.disconnect()\n      stopTheme()\n      document.removeEventListener(\"visibilitychange\", sync)\n    }\n  }, [gap, density, colorsKey, direction, speed, railOpacity])\n\n  const edge = direction === \"up\" ? \"100%\" : \"0%\"\n  const first = colors[0] ?? \"--chart-1\"\n  const glowColor = first.startsWith(\"--\") ? `var(${first})` : first\n  const fadeMask = \"linear-gradient(to bottom, transparent, black 18%, black 82%, transparent)\"\n\n  return (\n    <div\n      ref={rootRef}\n      aria-hidden=\"true\"\n      data-slot=\"beams-background\"\n      className={cn(\"pointer-events-none absolute inset-0 overflow-hidden\", className)}\n      {...props}\n    >\n      {glow && (\n        <div\n          data-slot=\"beams-background-glow\"\n          className=\"absolute inset-0\"\n          style={{\n            background: `radial-gradient(ellipse 55% 45% at 50% ${edge}, color-mix(in oklch, ${glowColor} 30%, transparent), transparent 70%)`,\n          }}\n        />\n      )}\n      <canvas ref={canvasRef} className=\"absolute inset-0 block\" style={{ maskImage: fadeMask, WebkitMaskImage: fadeMask }} />\n    </div>\n  )\n}\n\nexport { BeamsBackground, type BeamsBackgroundProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/beams-background.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "beams",
      "light",
      "rails",
      "canvas",
      "background",
      "hero",
      "waitlist"
    ],
    "version": "1.0.0",
    "updated": "2026-09-29",
    "ai": {
      "summary": "Place <BeamsBackground /> as the first child of a relative, overflow-hidden section; it fills it (absolute inset-0, aria-hidden, pointer-events-none). Content goes after it; the rails fade out at the top and bottom edges.",
      "whenToUse": [
        "Waitlist, sign-up or launch sections with centered content",
        "Hero backgrounds for developer tools, infrastructure or AI products"
      ],
      "whenNotToUse": [
        "Behind long text or tables",
        "Pages that already have another animated background"
      ],
      "composesWith": [
        "input",
        "button",
        "shimmer-text",
        "text-reveal"
      ],
      "a11y": [],
      "customization": [
        "gap: rail spacing in px (default 36); density: beams per 1000 px of width (default 30)",
        "colors: CSS variable names or colors, default --chart-1, --chart-2, --chart-4",
        "direction: up | down; speed multiplier (default 1); railOpacity (default 0.07)",
        "glow toggles the soft light (in the first color) at the edge the beams come from"
      ]
    },
    "examples": [
      "beams-background-demo",
      "beams-background-falling"
    ],
    "url": "https://ui.ballmac.com/components/beams-background"
  },
  "categories": [
    "backgrounds"
  ],
  "type": "registry:ui"
}