{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "beams-background",
  "title": "Beams Background",
  "description": "A canvas background of thin vertical rails with beams of light traveling along them over a soft glow.",
  "files": [
    {
      "path": "registry/tealmac/beams-background.tsx",
      "content": "// Adapted from Ballmac UI (https://github.com/vamsiy78/ballmac-ui), MIT License, Copyright (c) 2026 Ballmac.\n\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/** Reads a CSS variable name (\"--primary\"), a var() expression or any CSS color in the context of `element`. */\nfunction resolveCssColor(element: Element, color: string): string {\n  const style = getComputedStyle(element)\n  const name = color.startsWith(\"--\")\n    ? color\n    : /^var\\((--[^,)]+)/.exec(color)?.[1]\n  const value = name ? style.getPropertyValue(name).trim() : color\n  return value || style.color\n}\n\nlet probe: CanvasRenderingContext2D | null = null\n\n/** The color as [r, g, b, a] in 0-1, resolved by painting one pixel on a scratch canvas. */\nfunction cssColorToRgba(\n  element: Element,\n  color: string\n): [number, number, number, number] {\n  probe ??= document\n    .createElement(\"canvas\")\n    .getContext(\"2d\", { willReadFrequently: true })\n  if (!probe) return [0, 0, 0, 1]\n  probe.clearRect(0, 0, 1, 1)\n  probe.fillStyle = resolveCssColor(element, color)\n  probe.fillRect(0, 0, 1, 1)\n  const [r, g, b, a] = probe.getImageData(0, 0, 1, 1).data\n  return [r / 255, g / 255, b / 255, a / 255]\n}\n\n/** Calls `onChange` when the theme switches (the `dark` class or a data-theme attribute on <html>). */\nfunction observeTheme(onChange: () => void): () => void {\n  const observer = new MutationObserver(onChange)\n  observer.observe(document.documentElement, {\n    attributes: true,\n    attributeFilter: [\"class\", \"data-theme\", \"style\"],\n  })\n  const media = window.matchMedia(\"(prefers-color-scheme: dark)\")\n  media.addEventListener(\"change\", onChange)\n  return () => {\n    observer.disconnect()\n    media.removeEventListener(\"change\", onChange)\n  }\n}\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 = {\n  col: number\n  y: number\n  length: number\n  speed: number\n  color: number\n  width: number\n  life: number\n}\n\nfunction BeamsBackground({\n  gap = 36,\n  density = 30,\n  colors = [\"--chart-1\", \"--chart-3\", \"--chart-2\"],\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(\n      \"(prefers-reduced-motion: reduce)\"\n    ).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(\n        (c) =>\n          cssColorToRgba(root, c)\n            .slice(0, 3)\n            .map((v) => Math.round(v * 255)) as [number, number, number]\n      )\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 =\n        direction === \"up\"\n          ? beam.y - beam.length\n          : 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      // NaN when the canvas has no height yet; addColorStop would throw.\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 =\n    \"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(\n        \"pointer-events-none absolute inset-0 overflow-hidden\",\n        className\n      )}\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\n        ref={canvasRef}\n        className=\"absolute inset-0 block\"\n        style={{ maskImage: fadeMask, WebkitMaskImage: fadeMask }}\n      />\n    </div>\n  )\n}\n\nexport { BeamsBackground, type BeamsBackgroundProps }\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}