{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "warp-background",
  "title": "Warp Background",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A hyperspace tunnel of streaks flying out from the center on a canvas, in theme colors with a soft vignette for readable content, parallax toward the pointer, pausing off screen.",
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/color.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/warp-background.tsx",
      "content": "// Ballmac UI: Warp Background. https://ui.ballmac.com/components/warp-background\n\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { observeTheme, resolveCssColor } from \"@/lib/ballmac/color\"\n\ntype WarpBackgroundProps = Omit<React.ComponentProps<\"div\">, \"color\"> & {\n  /** How many streaks fly past at once. */\n  density?: number\n  /** How fast they travel. 1 is a steady cruise. */\n  speed?: number\n  /** Colors of the streaks: CSS variable names such as \"--chart-1\", or any CSS colors. Streaks pick from these at random. */\n  colors?: string[]\n  /** Darken the middle so content on top stays readable. */\n  vignette?: boolean\n  /** The vanishing point follows the pointer a little. */\n  parallax?: boolean\n  /** Classes for the layer that holds the children. */\n  contentClassName?: string\n}\n\ntype Star = { angle: number; radius: number; speed: number; color: number; length: number }\n\n/** A hyperspace tunnel: streaks fly outward from the center on a canvas, with your content on top. */\nfunction WarpBackground({\n  density = 240,\n  speed = 1,\n  colors = [\"--foreground\", \"--chart-1\", \"--chart-4\"],\n  vignette = true,\n  parallax = true,\n  className,\n  contentClassName,\n  children,\n  ...props\n}: WarpBackgroundProps) {\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  const canvasRef = React.useRef<HTMLCanvasElement>(null)\n  const colorKey = 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    let width = 0\n    let height = 0\n    let dpr = 1\n    let palette = colors.map((c) => resolveCssColor(root, c))\n    let stars: Star[] = []\n    let cx = 0.5\n    let cy = 0.5\n    let targetX = 0.5\n    let targetY = 0.5\n    let frame = 0\n    let last = 0\n    let visible = true\n\n    const spawn = (initial: boolean): Star => ({\n      angle: Math.random() * Math.PI * 2,\n      radius: initial ? Math.random() ** 2 * 0.9 : 0.01 + Math.random() * 0.03,\n      speed: 0.25 + Math.random() * 0.6,\n      color: Math.floor(Math.random() * palette.length),\n      length: 0.6 + Math.random() * 1.4,\n    })\n\n    const resize = () => {\n      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    }\n\n    const draw = (dt: number) => {\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0)\n      ctx.clearRect(0, 0, width, height)\n      const reach = Math.hypot(width, height) / 2\n      const ox = cx * width\n      const oy = cy * height\n      ctx.lineCap = \"round\"\n      for (let i = 0; i < stars.length; i++) {\n        const s = stars[i]!\n        const before = s.radius\n        s.radius += dt * speed * s.speed * (0.12 + s.radius * 1.9)\n        if (s.radius * reach > reach * 1.1) {\n          stars[i] = spawn(false)\n          continue\n        }\n        const tail = Math.max(before, s.radius - dt * speed * s.speed * (0.12 + s.radius * 1.9) * s.length * 3)\n        const cos = Math.cos(s.angle)\n        const sin = Math.sin(s.angle)\n        ctx.globalAlpha = Math.min(1, s.radius * 2.2) * 0.9\n        ctx.strokeStyle = palette[s.color % palette.length]!\n        ctx.lineWidth = 0.6 + s.radius * 2.2\n        ctx.beginPath()\n        ctx.moveTo(ox + cos * tail * reach, oy + sin * tail * reach)\n        ctx.lineTo(ox + cos * s.radius * reach, oy + sin * s.radius * reach)\n        ctx.stroke()\n      }\n      ctx.globalAlpha = 1\n    }\n\n    const loop = (now: number) => {\n      const dt = last ? Math.min((now - last) / 1000, 0.05) : 0\n      last = now\n      cx += (targetX - cx) * 0.06\n      cy += (targetY - cy) * 0.06\n      draw(dt)\n      frame = requestAnimationFrame(loop)\n    }\n    const sync = () => {\n      cancelAnimationFrame(frame)\n      last = 0\n      if (reduceMotion) draw(0.016 * 8)\n      else if (visible && !document.hidden) frame = requestAnimationFrame(loop)\n    }\n    const onMove = (e: PointerEvent) => {\n      if (!parallax || reduceMotion) return\n      const box = root.getBoundingClientRect()\n      targetX = 0.5 + ((e.clientX - box.left) / box.width - 0.5) * 0.14\n      targetY = 0.5 + ((e.clientY - box.top) / box.height - 0.5) * 0.14\n    }\n\n    resize()\n    stars = Array.from({ length: density }, () => spawn(true))\n    sync()\n    root.addEventListener(\"pointermove\", onMove)\n    const ro = new ResizeObserver(() => {\n      resize()\n      if (reduceMotion) draw(0.1)\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      palette = colors.map((c) => resolveCssColor(root, c))\n      if (reduceMotion) draw(0.1)\n    })\n    document.addEventListener(\"visibilitychange\", sync)\n    return () => {\n      cancelAnimationFrame(frame)\n      root.removeEventListener(\"pointermove\", onMove)\n      ro.disconnect()\n      io.disconnect()\n      stopTheme()\n      document.removeEventListener(\"visibilitychange\", sync)\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [density, speed, colorKey, parallax])\n\n  return (\n    <div ref={rootRef} data-slot=\"warp-background\" className={cn(\"relative isolate overflow-hidden bg-background\", className)} {...props}>\n      <canvas ref={canvasRef} aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0 size-full\" />\n      {vignette && (\n        <span aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_45%_45%_at_50%_50%,var(--background),transparent_75%)] opacity-80\" />\n      )}\n      <div className={cn(\"relative z-10\", contentClassName)}>{children}</div>\n    </div>\n  )\n}\n\nexport { WarpBackground, type WarpBackgroundProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/warp-background.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "warp",
      "hyperspace",
      "canvas",
      "background",
      "hero"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "<WarpBackground density speed colors vignette parallax>content</WarpBackground>. colors takes CSS variable names. The content sits on its own layer above the canvas.",
      "whenToUse": [
        "Launch and waitlist pages",
        "A dramatic backdrop for one centered message"
      ],
      "whenNotToUse": [
        "Content-heavy pages",
        "Anything that must be calm all the time"
      ],
      "composesWith": [
        "particles",
        "retro-grid",
        "light-rays"
      ],
      "a11y": [
        {
          "keys": "Screen readers",
          "action": "The canvas is aria-hidden; children are read normally"
        },
        {
          "keys": "Contrast",
          "action": "The vignette darkens the center behind text"
        },
        {
          "keys": "Reduced motion",
          "action": "A single still frame of streaks"
        }
      ],
      "customization": [
        "density",
        "speed",
        "colors",
        "vignette",
        "parallax"
      ]
    },
    "examples": [
      "warp-background-demo",
      "warp-background-cards"
    ],
    "url": "https://ui.ballmac.com/components/warp-background"
  },
  "categories": [
    "backgrounds"
  ],
  "type": "registry:ui"
}