{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "grid-beams",
  "type": "registry:ui",
  "title": "Grid Beams",
  "description": "A ruled grid with neon beams running down random lines, fading out toward the edges.",
  "registryDependencies": [
    "utils",
    "use-in-viewport"
  ],
  "files": [
    {
      "path": "registry/loomui/grid-beams.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useInViewport } from \"@/registry/lib/use-in-viewport\"\n\nexport interface GridBeamsProps extends React.ComponentProps<\"div\"> {\n  /** Grid cell size in pixels. */\n  cellSize?: number\n  /** How many beams travel at once. */\n  beams?: number\n  /** Colours the beams are drawn from, one picked per beam. */\n  colors?: string[]\n  /** Seconds for one beam to cross the container. */\n  duration?: number\n  /** Length of a beam, as a percentage of the container it crosses. */\n  length?: string\n  /** Thickness of a beam in pixels. */\n  thickness?: number\n  /** Opacity of the static grid lines. */\n  lineOpacity?: number\n  /** Which way beams travel. `both` mixes the two. */\n  axis?: \"vertical\" | \"horizontal\" | \"both\"\n  /** Fade the layer out. `true` fades from the top, or pass a CSS mask. */\n  fade?: boolean | string\n  /** Changes which lines are chosen. Same seed, same layout, every render. */\n  seed?: number\n  /** Render the grid with no beams. */\n  disabled?: boolean\n}\n\nconst DEFAULT_COLORS = [\"#22d3ee\", \"#38bdf8\", \"#a855f7\", \"#f472b6\"]\n\n/** Matches the hero: bright at the top, gone by the middle. */\n/** Rounded off rather than ramped: a two-stop ramp meets full opacity at a\n *  point the eye reads as a hard line. */\nconst TOP_FADE =\n  \"radial-gradient(ellipse 75% 55% at 50% 0%, black 30%, rgba(0,0,0,0.5) 65%, transparent 100%)\"\n\n/** Mulberry32. Deterministic so a seed always gives the same layout. */\nfunction createRandom(seed: number) {\n  let state = seed >>> 0\n  return () => {\n    state = (state + 0x6d2b79f5) >>> 0\n    let t = state\n    t = Math.imul(t ^ (t >>> 15), t | 1)\n    t ^= t + Math.imul(t ^ (t >>> 7), t | 61)\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296\n  }\n}\n\nexport function GridBeams({\n  cellSize = 64,\n  beams = 7,\n  colors = DEFAULT_COLORS,\n  duration = 4,\n  length = \"24%\",\n  thickness = 2,\n  lineOpacity = 0.05,\n  axis = \"both\",\n  fade = true,\n  seed = 1,\n  disabled = false,\n  className,\n  style,\n  ...props\n}: GridBeamsProps) {\n  const ref = React.useRef<HTMLDivElement>(null)\n  const [box, setBox] = React.useState({ width: 0, height: 0 })\n\n  // Grid lines land on fixed pixel multiples, so which of them are actually\n  // on screen depends on how big the container is. Measuring is what keeps\n  // every beam inside the frame instead of scattering some past the edge.\n  React.useEffect(() => {\n    const node = ref.current\n    if (!node) {\n      return\n    }\n\n    const observer = new ResizeObserver(([entry]) => {\n      const { width, height } = entry.contentRect\n      setBox((previous) =>\n        previous.width === width && previous.height === height\n          ? previous\n          : { width, height }\n      )\n    })\n\n    observer.observe(node)\n    return () => observer.disconnect()\n  }, [])\n\n  const streaks = React.useMemo(() => {\n    const columns = Math.floor(box.width / cellSize)\n    const rows = Math.floor(box.height / cellSize)\n\n    if (disabled || beams <= 0) {\n      return []\n    }\n\n    const palette = colors.length > 0 ? colors : DEFAULT_COLORS\n    const random = createRandom(seed)\n\n    // The layer clips, so a beam on the first or last line would be sliced in\n    // half against the edge. The choice runs over the lines in between, and a\n    // container too small to have any is left with no beams at all.\n    const pickLine = (count: number) =>\n      count < 3 ? null : 1 + Math.floor(random() * (count - 2))\n\n    return Array.from({ length: beams }, (_, index) => {\n      // A third horizontal is enough to read as a circuit rather than as\n      // rain. An even split fights itself for attention.\n      const horizontal =\n        axis === \"horizontal\" || (axis === \"both\" && random() < 0.35)\n\n      return {\n        key: index,\n        horizontal,\n        line: pickLine(horizontal ? rows : columns),\n        color: palette[Math.floor(random() * palette.length)],\n        duration: duration * (0.7 + random() * 0.8),\n        // Negative, so a beam is already partway along on the first frame\n        // instead of every beam launching from the edge together.\n        delay: -random() * duration * 2,\n      }\n    }).filter((streak) => streak.line !== null)\n  }, [\n    disabled,\n    beams,\n    colors,\n    duration,\n    axis,\n    seed,\n    cellSize,\n    box.width,\n    box.height,\n  ])\n\n  const onScreen = useInViewport(ref)\n  const mask = fade === true ? TOP_FADE : fade === false ? undefined : fade\n\n  return (\n    <div\n      ref={ref}\n      data-slot=\"grid-beams\"\n      aria-hidden=\"true\"\n      className={cn(\n        \"pointer-events-none absolute inset-0 overflow-hidden\",\n        className\n      )}\n      style={{ WebkitMaskImage: mask, maskImage: mask, ...style }}\n      {...props}\n    >\n      {/* Two gradients crossed: one draws the verticals, one the horizontals.\n          `currentColor` so the lines inherit the surface's own ink instead of\n          carrying a colour that only works on one theme. */}\n      <div\n        className=\"absolute inset-0\"\n        style={{\n          backgroundImage:\n            \"linear-gradient(to right, currentColor 1px, transparent 1px), linear-gradient(to bottom, currentColor 1px, transparent 1px)\",\n          backgroundSize: `${cellSize}px ${cellSize}px`,\n          opacity: lineOpacity,\n        }}\n      />\n\n      {streaks.map((streak) => {\n        // The element spans the whole axis it travels, with the streak painted\n        // into the leading slice of it. Translating by a percentage is then a\n        // percentage of the container, so the travel itself needs no\n        // measurement and stays on the compositor.\n        const along: React.CSSProperties = streak.horizontal\n          ? {\n              top: `${streak.line! * cellSize}px`,\n              width: \"100%\",\n              height: thickness,\n              background: `linear-gradient(to right, transparent 0%, ${streak.color} calc(${length} * 0.85), transparent ${length})`,\n            }\n          : {\n              left: `${streak.line! * cellSize}px`,\n              width: thickness,\n              height: \"100%\",\n              background: `linear-gradient(to bottom, transparent 0%, ${streak.color} calc(${length} * 0.85), transparent ${length})`,\n            }\n\n        return (\n          <span\n            key={streak.key}\n            className={cn(\n              \"absolute motion-reduce:hidden\",\n              streak.horizontal\n                ? \"animate-grid-beam-x left-0\"\n                : \"animate-grid-beam top-0\",\n              !onScreen && \"[animation-play-state:paused]\"\n            )}\n            style={\n              {\n                ...along,\n                \"--beam-length\": length,\n                // Drop-shadow reads the gradient's alpha, so the glow follows\n                // the streak rather than boxing the whole element.\n                filter: `drop-shadow(0 0 6px ${streak.color})`,\n                animationDuration: `${streak.duration}s`,\n                animationDelay: `${streak.delay}s`,\n              } as React.CSSProperties\n            }\n          />\n        )\n      })}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "cssVars": {
    "theme": {
      "animate-grid-beam": "grid-beam 4s linear infinite",
      "animate-grid-beam-x": "grid-beam-x 4s linear infinite"
    }
  },
  "css": {
    "@keyframes grid-beam": {
      "from": {
        "transform": "translateY(calc(-1 * var(--beam-length, 24%)))"
      },
      "to": {
        "transform": "translateY(100%)"
      }
    },
    "@keyframes grid-beam-x": {
      "from": {
        "transform": "translateX(calc(-1 * var(--beam-length, 24%)))"
      },
      "to": {
        "transform": "translateX(100%)"
      }
    }
  }
}