{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hero-background",
  "title": "Hero Background",
  "description": "Interactive canvas backgrounds for hero sections: dots, warping grid, constellation particles, waves, flow field and pixel mosaic, all reacting to the cursor.",
  "dependencies": [],
  "registryDependencies": [
    "utils",
    "https://trytweenly.vercel.app/r/use-reduced-motion.json"
  ],
  "files": [
    {
      "path": "registry/new-york/hero-background/hero-background.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, type CSSProperties, type ReactNode } from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { useReducedMotion } from \"@/registry/new-york/hooks/use-reduced-motion\"\n\n// ---------------------------------------------------------------- types\n\nexport type HeroBackgroundVariant = \"dots\" | \"grid\" | \"particles\" | \"waves\" | \"flow\" | \"pixels\"\nexport type HeroBackgroundInteraction = \"repel\" | \"attract\" | \"none\"\nexport type HeroBackgroundMask = \"none\" | \"radial\" | \"fade-bottom\" | \"fade-edges\"\n\nexport interface HeroBackgroundProps {\n  /** Visual style of the background. Default: \"dots\" */\n  variant?: HeroBackgroundVariant\n  /** Primary mark color (any CSS color, including CSS variables). Follows the text color by default. Default: \"currentColor\" */\n  color?: string\n  /** Highlight color used near the cursor (any CSS color). Default: \"#ff4d12\" */\n  accent?: string\n  /** Background fill behind the marks (any CSS color). Default: \"transparent\" */\n  background?: string\n  /** Mark density multiplier. Higher packs dots, lines, cells and particles closer together. Default: 1 */\n  density?: number\n  /** Mark size in px: dot radius, line width, particle radius, or the gap between pixel cells. Default: 1.5 */\n  size?: number\n  /** Animation speed multiplier for ambient motion. Default: 1 */\n  speed?: number\n  /** How marks react to the cursor. \"none\" only highlights. Default: \"repel\" */\n  interaction?: HeroBackgroundInteraction\n  /** Cursor influence radius in px. Default: 160 */\n  radius?: number\n  /** Strength of the cursor push, pull and highlight. Default: 1 */\n  strength?: number\n  /** Opacity of the resting marks, 0 to 1. Marks near the cursor brighten towards 1. Default: 0.35 */\n  opacity?: number\n  /** CSS mask applied to the canvas so foreground content stays readable. Default: \"none\" */\n  mask?: HeroBackgroundMask\n  /** Emit a ripple from the pointer on click or tap. Default: true */\n  clickRipple?: boolean\n  /** React to the pointer at all. Default: true */\n  interactive?: boolean\n  /** Seed for the deterministic layout and noise. Default: 1 */\n  seed?: number\n  /** Frame rate cap. Default: 60 */\n  fps?: number\n  /** Hero content rendered on top of the background. */\n  children?: ReactNode\n  className?: string\n}\n\n// ---------------------------------------------------------------- engine\n\n/** Number of accent-mix levels for marks near the cursor. */\nconst LEVELS = 6\n/** Number of resting alpha steps used for ambient shimmer and fades. */\nconst FADE = 4\n/** Palette size: FADE resting steps followed by LEVELS accent mixes. */\nconst NB = FADE + LEVELS\nconst BASE = FADE // palette index of a resting mark at full `opacity`\nconst TAU = Math.PI * 2\nconst RIPPLES = 4\nconst RIPPLE_SPEED = 620 // px per second\nconst RIPPLE_LIFE = 1.6 // seconds\nconst RIPPLE_WIDTH = 34\n\ninterface Live {\n  color: string\n  accent: string\n  size: number\n  speed: number\n  sign: number // 1 repel, -1 attract, 0 none\n  radius: number\n  strength: number\n  opacity: number\n  clickRipple: boolean\n  fps: number\n}\n\ninterface World {\n  ctx: CanvasRenderingContext2D\n  w: number\n  h: number\n  /** Animation clock, scaled by `speed`. */\n  t: number\n  px: number\n  py: number\n  pvx: number\n  pvy: number\n  presence: number\n  rip: Float32Array\n  rippling: boolean\n  /** Direction output of rippleAt(). */\n  rx: number\n  ry: number\n  o: Live\n  pal: string[]\n  hl: string[]\n  rng: () => number\n  noise: (x: number, y: number) => number\n  density: number\n  counts: Int32Array\n}\n\ninterface Scene {\n  step(dt: number): void\n  draw(): void\n}\n\ntype SceneFactory = (W: World) => Scene\n\nfunction mulberry32(seed: number) {\n  let a = seed >>> 0\n  return () => {\n    a = (a + 0x6d2b79f5) >>> 0\n    let t = a\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\n/** Seeded 2D value noise in the 0..1 range. */\nfunction makeNoise(rng: () => number) {\n  const perm = new Uint8Array(512)\n  const vals = new Float32Array(256)\n  for (let i = 0; i < 256; i++) {\n    perm[i] = i\n    vals[i] = rng()\n  }\n  for (let i = 255; i > 0; i--) {\n    const j = Math.floor(rng() * (i + 1))\n    const tmp = perm[i]\n    perm[i] = perm[j]\n    perm[j] = tmp\n  }\n  for (let i = 0; i < 256; i++) perm[i + 256] = perm[i]\n  return (x: number, y: number) => {\n    const xf0 = Math.floor(x)\n    const yf0 = Math.floor(y)\n    const xi = xf0 & 255\n    const yi = yf0 & 255\n    const fx = x - xf0\n    const fy = y - yf0\n    const u = fx * fx * (3 - 2 * fx)\n    const v = fy * fy * (3 - 2 * fy)\n    const a = vals[perm[perm[xi] + yi]]\n    const b = vals[perm[perm[xi + 1] + yi]]\n    const c = vals[perm[perm[xi] + yi + 1]]\n    const d = vals[perm[perm[xi + 1] + yi + 1]]\n    const top = a + (b - a) * u\n    return top + (c + (d - c) * u - top) * v\n  }\n}\n\n/** Smooth 0..1 cursor influence at a point. */\nfunction influence(W: World, x: number, y: number) {\n  if (W.presence < 0.002) return 0\n  const R = W.o.radius\n  const dx = x - W.px\n  const dy = y - W.py\n  const d2 = dx * dx + dy * dy\n  if (d2 >= R * R) return 0\n  const k = 1 - Math.sqrt(d2) / R\n  return k * k * (3 - 2 * k) * W.presence\n}\n\n/** Ripple intensity at a point; writes the outward direction to W.rx / W.ry. */\nfunction rippleAt(W: World, x: number, y: number) {\n  W.rx = 0\n  W.ry = 0\n  if (!W.rippling) return 0\n  let s = 0\n  for (let i = 0; i < RIPPLES; i++) {\n    const age = W.rip[i * 3 + 2]\n    if (age < 0) continue\n    const dx = x - W.rip[i * 3]\n    const dy = y - W.rip[i * 3 + 1]\n    const dist = Math.sqrt(dx * dx + dy * dy) || 1\n    const off = dist - age * RIPPLE_SPEED\n    if (off > RIPPLE_WIDTH * 3 || off < -RIPPLE_WIDTH * 3) continue\n    const g = Math.exp(-(off * off) / (2 * RIPPLE_WIDTH * RIPPLE_WIDTH)) * (1 - age / RIPPLE_LIFE)\n    s += g\n    W.rx += (dx / dist) * g\n    W.ry += (dy / dist) * g\n  }\n  return s\n}\n\n/** Palette index for an influence value: accent mix when lit, otherwise `rest`. */\nfunction paletteIndex(f: number, rest: number) {\n  if (f <= 0.03) return rest\n  return BASE + Math.min(LEVELS - 1, 1 + Math.floor(f * (LEVELS - 1)))\n}\n\nfunction resetCounts(W: World) {\n  W.counts.fill(0)\n}\n\n// ---------------------------------------------------------------- dots\n\nconst dots: SceneFactory = (W) => {\n  const sp = Math.max(8, 26 / W.density)\n  const cols = Math.ceil(W.w / sp) + 1\n  const rows = Math.ceil(W.h / sp) + 1\n  const n = cols * rows\n  const x0 = (W.w - (cols - 1) * sp) / 2\n  const y0 = (W.h - (rows - 1) * sp) / 2\n  const hx = new Float32Array(n)\n  const hy = new Float32Array(n)\n  const ox = new Float32Array(n)\n  const oy = new Float32Array(n)\n  const vx = new Float32Array(n)\n  const vy = new Float32Array(n)\n  const sc = new Float32Array(n)\n  const lv = new Uint8Array(n)\n  for (let j = 0; j < rows; j++) {\n    for (let i = 0; i < cols; i++) {\n      hx[j * cols + i] = x0 + i * sp\n      hy[j * cols + i] = y0 + j * sp\n    }\n  }\n\n  return {\n    step(dt) {\n      const o = W.o\n      const push = o.strength * o.radius * 0.22 * o.sign\n      const k = 170 * dt\n      const damp = Math.exp(-13 * dt)\n      const ease = 1 - Math.exp(-dt * 10)\n      const ns = 0.006\n      for (let i = 0; i < n; i++) {\n        const x = hx[i]\n        const y = hy[i]\n        const f = influence(W, x, y)\n        const r = rippleAt(W, x, y)\n        let tx = 0\n        let ty = 0\n        if (f > 0) {\n          const dx = x - W.px\n          const dy = y - W.py\n          const d = Math.sqrt(dx * dx + dy * dy) || 1\n          let m = f * push\n          if (m < -d * 0.8) m = -d * 0.8\n          tx = (dx / d) * m\n          ty = (dy / d) * m\n        }\n        if (r > 0) {\n          tx += W.rx * sp * 0.5 * o.strength\n          ty += W.ry * sp * 0.5 * o.strength\n        }\n        vx[i] = (vx[i] + (tx - ox[i]) * k) * damp\n        vy[i] = (vy[i] + (ty - oy[i]) * k) * damp\n        ox[i] += vx[i] * dt\n        oy[i] += vy[i] * dt\n        const lit = Math.min(1, (f + r * 0.8) * Math.min(1.4, o.strength))\n        sc[i] += (lit * 1.3 - sc[i]) * ease\n        const amb = W.noise(x * ns + W.t * 0.12, y * ns - W.t * 0.05)\n        lv[i] = paletteIndex(lit, 1 + Math.min(2, Math.floor(amb * 3)))\n      }\n    },\n    draw() {\n      const ctx = W.ctx\n      const base = W.o.size\n      resetCounts(W)\n      for (let i = 0; i < n; i++) W.counts[lv[i]]++\n      for (let p = 0; p < NB; p++) {\n        if (!W.counts[p]) continue\n        ctx.fillStyle = W.pal[p]\n        ctx.beginPath()\n        for (let i = 0; i < n; i++) {\n          if (lv[i] !== p) continue\n          const r = base * (1 + sc[i])\n          const x = hx[i] + ox[i]\n          const y = hy[i] + oy[i]\n          ctx.moveTo(x + r, y)\n          ctx.arc(x, y, r, 0, TAU)\n        }\n        ctx.fill()\n      }\n    },\n  }\n}\n\n// ---------------------------------------------------------------- grid\n\nconst grid: SceneFactory = (W) => {\n  const sp = Math.max(14, 44 / W.density)\n  const cols = Math.ceil(W.w / sp) + 3\n  const rows = Math.ceil(W.h / sp) + 3\n  const n = cols * rows\n  const x0 = (W.w - (cols - 1) * sp) / 2\n  const y0 = (W.h - (rows - 1) * sp) / 2\n  const ox = new Float32Array(n)\n  const oy = new Float32Array(n)\n  const vx = new Float32Array(n)\n  const vy = new Float32Array(n)\n  const nf = new Float32Array(n)\n  const segs = new Uint8Array(n * 2) // [h, v] per node\n\n  return {\n    step(dt) {\n      const o = W.o\n      const push = o.strength * o.radius * 0.24 * o.sign\n      const k = 150 * dt\n      const damp = Math.exp(-12 * dt)\n      for (let j = 0; j < rows; j++) {\n        for (let i = 0; i < cols; i++) {\n          const idx = j * cols + i\n          const x = x0 + i * sp\n          const y = y0 + j * sp\n          const f = influence(W, x, y)\n          const r = rippleAt(W, x, y)\n          let tx = 0\n          let ty = 0\n          if (f > 0) {\n            const dx = x - W.px\n            const dy = y - W.py\n            const d = Math.sqrt(dx * dx + dy * dy) || 1\n            let m = f * push\n            if (m < -d * 0.85) m = -d * 0.85\n            tx = (dx / d) * m\n            ty = (dy / d) * m\n          }\n          if (r > 0) {\n            tx += W.rx * sp * 0.35 * o.strength\n            ty += W.ry * sp * 0.35 * o.strength\n          }\n          vx[idx] = (vx[idx] + (tx - ox[idx]) * k) * damp\n          vy[idx] = (vy[idx] + (ty - oy[idx]) * k) * damp\n          ox[idx] += vx[idx] * dt\n          oy[idx] += vy[idx] * dt\n          nf[idx] = Math.min(1, (f + r * 0.7) * Math.min(1.4, o.strength))\n        }\n      }\n    },\n    draw() {\n      const ctx = W.ctx\n      // Highlighted cells near the cursor\n      if (W.presence > 0.01 || W.rippling) {\n        for (let p = 1; p < LEVELS; p++) {\n          let any = false\n          for (let j = 0; j < rows - 1; j++) {\n            for (let i = 0; i < cols - 1; i++) {\n              const a = j * cols + i\n              const f = (nf[a] + nf[a + 1] + nf[a + cols] + nf[a + cols + 1]) * 0.25\n              if (f <= 0.03 || Math.min(LEVELS - 1, 1 + Math.floor(f * (LEVELS - 1))) !== p) continue\n              if (!any) {\n                ctx.fillStyle = W.hl[p]\n                ctx.beginPath()\n                any = true\n              }\n              const b = a + 1\n              const c = a + cols + 1\n              const d = a + cols\n              ctx.moveTo(x0 + i * sp + ox[a], y0 + j * sp + oy[a])\n              ctx.lineTo(x0 + (i + 1) * sp + ox[b], y0 + j * sp + oy[b])\n              ctx.lineTo(x0 + (i + 1) * sp + ox[c], y0 + (j + 1) * sp + oy[c])\n              ctx.lineTo(x0 + i * sp + ox[d], y0 + (j + 1) * sp + oy[d])\n              ctx.closePath()\n            }\n          }\n          if (any) ctx.fill()\n        }\n      }\n\n      // Lines, one path per palette entry\n      resetCounts(W)\n      for (let j = 0; j < rows; j++) {\n        for (let i = 0; i < cols; i++) {\n          const a = j * cols + i\n          const ph = i < cols - 1 ? paletteIndex(Math.max(nf[a], nf[a + 1]), BASE) : 255\n          const pv = j < rows - 1 ? paletteIndex(Math.max(nf[a], nf[a + cols]), BASE) : 255\n          segs[a * 2] = ph\n          segs[a * 2 + 1] = pv\n          if (ph !== 255) W.counts[ph]++\n          if (pv !== 255) W.counts[pv]++\n        }\n      }\n      ctx.lineWidth = Math.max(0.5, W.o.size * 0.66)\n      for (let p = 0; p < NB; p++) {\n        if (!W.counts[p]) continue\n        ctx.strokeStyle = W.pal[p]\n        ctx.beginPath()\n        for (let j = 0; j < rows; j++) {\n          for (let i = 0; i < cols; i++) {\n            const a = j * cols + i\n            const ax = x0 + i * sp + ox[a]\n            const ay = y0 + j * sp + oy[a]\n            if (segs[a * 2] === p) {\n              ctx.moveTo(ax, ay)\n              ctx.lineTo(x0 + (i + 1) * sp + ox[a + 1], y0 + j * sp + oy[a + 1])\n            }\n            if (segs[a * 2 + 1] === p) {\n              ctx.moveTo(ax, ay)\n              ctx.lineTo(x0 + i * sp + ox[a + cols], y0 + (j + 1) * sp + oy[a + cols])\n            }\n          }\n        }\n        ctx.stroke()\n      }\n    },\n  }\n}\n\n// ---------------------------------------------------------------- particles\n\nconst MAX_SEGS = 6000\n\nconst particles: SceneFactory = (W) => {\n  const sp = Math.max(36, 110 / W.density)\n  const n = Math.max(12, Math.min(420, Math.round((W.w * W.h) / (sp * sp))))\n  const link = sp * 1.3\n  const x = new Float32Array(n)\n  const y = new Float32Array(n)\n  const vx = new Float32Array(n)\n  const vy = new Float32Array(n)\n  const bx = new Float32Array(n)\n  const by = new Float32Array(n)\n  const lv = new Uint8Array(n)\n  const seg = new Float32Array(MAX_SEGS * 4)\n  const segP = new Uint8Array(MAX_SEGS)\n  let segN = 0\n  for (let i = 0; i < n; i++) {\n    x[i] = W.rng() * W.w\n    y[i] = W.rng() * W.h\n    const a = W.rng() * TAU\n    const s = 6 + W.rng() * 14\n    bx[i] = Math.cos(a) * s\n    by[i] = Math.sin(a) * s\n    vx[i] = bx[i]\n    vy[i] = by[i]\n  }\n\n  const addSeg = (ax: number, ay: number, cx: number, cy: number, p: number) => {\n    if (segN >= MAX_SEGS) return\n    const o = segN * 4\n    seg[o] = ax\n    seg[o + 1] = ay\n    seg[o + 2] = cx\n    seg[o + 3] = cy\n    segP[segN] = p\n    segN++\n  }\n\n  return {\n    step(dt) {\n      const o = W.o\n      const relax = 1 - Math.exp(-dt * 1.5)\n      const accel = o.sign * o.strength * 900 * dt\n      const m = 20\n      for (let i = 0; i < n; i++) {\n        vx[i] += (bx[i] * o.speed - vx[i]) * relax\n        vy[i] += (by[i] * o.speed - vy[i]) * relax\n        const f = influence(W, x[i], y[i])\n        if (f > 0 && accel !== 0) {\n          const dx = x[i] - W.px\n          const dy = y[i] - W.py\n          const d = Math.sqrt(dx * dx + dy * dy) || 1\n          vx[i] += (dx / d) * f * accel\n          vy[i] += (dy / d) * f * accel\n        }\n        if (rippleAt(W, x[i], y[i]) > 0) {\n          vx[i] += W.rx * o.strength * 500 * dt\n          vy[i] += W.ry * o.strength * 500 * dt\n        }\n        x[i] += vx[i] * dt\n        y[i] += vy[i] * dt\n        if (x[i] < -m) x[i] += W.w + m * 2\n        else if (x[i] > W.w + m) x[i] -= W.w + m * 2\n        if (y[i] < -m) y[i] += W.h + m * 2\n        else if (y[i] > W.h + m) y[i] -= W.h + m * 2\n        lv[i] = paletteIndex(Math.min(1, f * o.strength), BASE)\n      }\n\n      // Links between close particles, faded by distance\n      segN = 0\n      const l2 = link * link\n      for (let i = 0; i < n; i++) {\n        for (let j = i + 1; j < n; j++) {\n          const dx = x[i] - x[j]\n          const dy = y[i] - y[j]\n          const d2 = dx * dx + dy * dy\n          if (d2 >= l2) continue\n          const fade = 1 - Math.sqrt(d2) / link\n          const f = influence(W, (x[i] + x[j]) * 0.5, (y[i] + y[j]) * 0.5)\n          const rest = Math.min(FADE - 1, Math.floor(fade * fade * FADE))\n          addSeg(x[i], y[i], x[j], y[j], f > 0.03 ? paletteIndex(f * fade * o.strength, rest) : rest)\n        }\n      }\n      // Links from the cursor\n      if (W.presence > 0.01) {\n        const r2 = o.radius * o.radius * 0.64\n        for (let i = 0; i < n; i++) {\n          const dx = x[i] - W.px\n          const dy = y[i] - W.py\n          if (dx * dx + dy * dy >= r2) continue\n          addSeg(W.px, W.py, x[i], y[i], paletteIndex(influence(W, x[i], y[i]) * Math.min(1, o.strength), 0))\n        }\n      }\n    },\n    draw() {\n      const ctx = W.ctx\n      resetCounts(W)\n      for (let s = 0; s < segN; s++) W.counts[segP[s]]++\n      ctx.lineWidth = Math.max(0.5, W.o.size * 0.5)\n      for (let p = 0; p < NB; p++) {\n        if (!W.counts[p]) continue\n        ctx.strokeStyle = W.pal[p]\n        ctx.beginPath()\n        for (let s = 0; s < segN; s++) {\n          if (segP[s] !== p) continue\n          const o = s * 4\n          ctx.moveTo(seg[o], seg[o + 1])\n          ctx.lineTo(seg[o + 2], seg[o + 3])\n        }\n        ctx.stroke()\n      }\n      resetCounts(W)\n      for (let i = 0; i < n; i++) W.counts[lv[i]]++\n      const r = W.o.size\n      for (let p = 0; p < NB; p++) {\n        if (!W.counts[p]) continue\n        ctx.fillStyle = W.pal[p]\n        ctx.beginPath()\n        for (let i = 0; i < n; i++) {\n          if (lv[i] !== p) continue\n          const rr = p > BASE ? r * 1.6 : r\n          ctx.moveTo(x[i] + rr, y[i])\n          ctx.arc(x[i], y[i], rr, 0, TAU)\n        }\n        ctx.fill()\n      }\n    },\n  }\n}\n\n// ---------------------------------------------------------------- waves\n\nconst waves: SceneFactory = (W) => {\n  const sp = Math.max(8, 26 / W.density)\n  const lines = Math.max(4, Math.min(80, Math.round(W.h / sp)))\n  const stepX = 10\n  const pts = Math.ceil(W.w / stepX) + 2\n  const gap = W.h / lines\n  const f1 = new Float32Array(lines)\n  const f2 = new Float32Array(lines)\n  const s1 = new Float32Array(lines)\n  const s2 = new Float32Array(lines)\n  const p1 = new Float32Array(lines)\n  const p2 = new Float32Array(lines)\n  const amp = new Float32Array(lines)\n  const disp = new Float32Array(lines * pts)\n  const ys = new Float32Array(lines * pts)\n  const pi = new Uint8Array(lines * pts)\n  for (let l = 0; l < lines; l++) {\n    f1[l] = 0.004 + W.rng() * 0.004\n    f2[l] = 0.009 + W.rng() * 0.008\n    s1[l] = 0.4 + W.rng() * 0.5\n    s2[l] = 0.3 + W.rng() * 0.6\n    p1[l] = W.rng() * TAU\n    p2[l] = W.rng() * TAU\n    amp[l] = gap * (0.5 + W.rng() * 0.6)\n  }\n\n  return {\n    step(dt) {\n      const o = W.o\n      const t = W.t\n      const ease = 1 - Math.exp(-dt * 8)\n      const push = o.sign * o.strength * o.radius * 0.28\n      const swell = o.strength * gap * 1.2\n      for (let l = 0; l < lines; l++) {\n        const base = (l + 0.5) * gap\n        // Neighbouring lines share a slow drift so the stack reads as one surface\n        const shared = W.noise(l * 0.15, t * 0.1) - 0.5\n        for (let k = 0; k < pts; k++) {\n          const xx = k * stepX\n          const wave =\n            Math.sin(xx * f1[l] + t * s1[l] + p1[l]) * 0.6 * amp[l] +\n            Math.sin(xx * f2[l] - t * s2[l] + p2[l]) * 0.4 * amp[l] +\n            shared * gap * 2\n          const yy = base + wave\n          const f = influence(W, xx, yy)\n          const r = rippleAt(W, xx, yy)\n          let target = r * 16 * o.strength * (W.ry >= 0 ? 1 : -1)\n          if (f > 0) {\n            const dir = yy >= W.py ? 1 : -1\n            target += dir * f * push + f * swell * Math.sin(xx * 0.045 - t * 5)\n          }\n          const i = l * pts + k\n          disp[i] += (target - disp[i]) * ease\n          ys[i] = yy + disp[i]\n          pi[i] = paletteIndex(Math.min(1, (f + r * 0.7) * Math.min(1.4, o.strength)), BASE)\n        }\n      }\n    },\n    draw() {\n      const ctx = W.ctx\n      ctx.lineWidth = Math.max(0.5, W.o.size * 0.7)\n      ctx.lineJoin = \"round\"\n      ctx.lineCap = \"round\"\n      ctx.strokeStyle = W.pal[BASE]\n      ctx.beginPath()\n      for (let l = 0; l < lines; l++) {\n        const row = l * pts\n        ctx.moveTo(0, ys[row])\n        for (let k = 1; k < pts; k++) ctx.lineTo(k * stepX, ys[row + k])\n      }\n      ctx.stroke()\n      // Accent overlay on lit segments\n      resetCounts(W)\n      for (let i = 0; i < pi.length; i++) W.counts[pi[i]]++\n      ctx.lineWidth = Math.max(0.5, W.o.size)\n      for (let p = BASE + 1; p < NB; p++) {\n        if (!W.counts[p]) continue\n        ctx.strokeStyle = W.pal[p]\n        ctx.beginPath()\n        for (let l = 0; l < lines; l++) {\n          const row = l * pts\n          for (let k = 0; k < pts - 1; k++) {\n            if (pi[row + k] !== p) continue\n            ctx.moveTo(k * stepX, ys[row + k])\n            ctx.lineTo((k + 1) * stepX, ys[row + k + 1])\n          }\n        }\n        ctx.stroke()\n      }\n    },\n  }\n}\n\n// ---------------------------------------------------------------- flow\n\nconst TRAIL = 12\n\nconst flow: SceneFactory = (W) => {\n  const sp = Math.max(10, 28 / W.density)\n  const n = Math.max(50, Math.min(1400, Math.round((W.w * W.h) / (sp * sp))))\n  const x = new Float32Array(n)\n  const y = new Float32Array(n)\n  const age = new Float32Array(n)\n  const life = new Float32Array(n)\n  const pi = new Uint8Array(n)\n  const trail = new Float32Array(n * TRAIL * 2)\n  let head = 0\n  let filled = 0\n  const ns = 0.0022\n\n  const spawn = (i: number, randomAge: boolean) => {\n    x[i] = W.rng() * W.w\n    y[i] = W.rng() * W.h\n    life[i] = 2 + W.rng() * 4\n    age[i] = randomAge ? W.rng() * life[i] : 0\n    const o = i * TRAIL * 2\n    for (let k = 0; k < TRAIL; k++) {\n      trail[o + k * 2] = x[i]\n      trail[o + k * 2 + 1] = y[i]\n    }\n  }\n  for (let i = 0; i < n; i++) spawn(i, true)\n\n  return {\n    step(dt) {\n      const o = W.o\n      const t = W.t\n      const v = 55 * o.speed\n      head = (head + 1) % TRAIL\n      if (filled < TRAIL) filled++\n      const m = 10\n      for (let i = 0; i < n; i++) {\n        age[i] += dt * Math.max(0.2, o.speed)\n        if (age[i] > life[i] || x[i] < -m || x[i] > W.w + m || y[i] < -m || y[i] > W.h + m) {\n          spawn(i, false)\n        }\n        const a = (W.noise(x[i] * ns + t * 0.03, y[i] * ns - t * 0.02) * 2 + W.noise(x[i] * ns * 3, y[i] * ns * 3 + t * 0.05) * 0.5) * TAU\n        let dx = Math.cos(a) * v\n        let dy = Math.sin(a) * v\n        const f = influence(W, x[i], y[i])\n        if (f > 0) {\n          const cx = x[i] - W.px\n          const cy = y[i] - W.py\n          const d = Math.sqrt(cx * cx + cy * cy) || 1\n          const s = f * o.strength\n          // Swirl around the cursor, push or pull radially, and drag along with the pointer\n          dx += (-cy / d) * s * 140 + (cx / d) * s * 110 * o.sign + W.pvx * s * 0.5\n          dy += (cx / d) * s * 140 + (cy / d) * s * 110 * o.sign + W.pvy * s * 0.5\n        }\n        if (rippleAt(W, x[i], y[i]) > 0) {\n          dx += W.rx * o.strength * 260\n          dy += W.ry * o.strength * 260\n        }\n        x[i] += dx * dt\n        y[i] += dy * dt\n        const off = (i * TRAIL + head) * 2\n        trail[off] = x[i]\n        trail[off + 1] = y[i]\n        const fadeIn = Math.min(1, age[i] / 0.6, (life[i] - age[i]) / 0.6)\n        pi[i] = paletteIndex(Math.min(1, f * o.strength), Math.max(0, Math.min(FADE - 1, Math.floor(fadeIn * FADE))))\n      }\n    },\n    draw() {\n      const ctx = W.ctx\n      resetCounts(W)\n      for (let i = 0; i < n; i++) W.counts[pi[i]]++\n      ctx.lineWidth = Math.max(0.5, W.o.size * 0.7)\n      ctx.lineCap = \"round\"\n      ctx.lineJoin = \"round\"\n      const len = filled\n      for (let p = 0; p < NB; p++) {\n        if (!W.counts[p]) continue\n        ctx.strokeStyle = W.pal[p]\n        ctx.beginPath()\n        for (let i = 0; i < n; i++) {\n          if (pi[i] !== p) continue\n          const base = i * TRAIL * 2\n          let k = (head - len + 1 + TRAIL) % TRAIL\n          ctx.moveTo(trail[base + k * 2], trail[base + k * 2 + 1])\n          for (let s = 1; s < len; s++) {\n            k = (k + 1) % TRAIL\n            ctx.lineTo(trail[base + k * 2], trail[base + k * 2 + 1])\n          }\n        }\n        ctx.stroke()\n      }\n    },\n  }\n}\n\n// ---------------------------------------------------------------- pixels\n\nconst pixels: SceneFactory = (W) => {\n  const cs = Math.max(6, 22 / W.density)\n  const cols = Math.ceil(W.w / cs)\n  const rows = Math.ceil(W.h / cs)\n  const n = cols * rows\n  const x0 = (W.w - cols * cs) / 2\n  const y0 = (W.h - rows * cs) / 2\n  const energy = new Float32Array(n)\n  const seedV = new Float32Array(n)\n  const pi = new Uint8Array(n)\n  for (let i = 0; i < n; i++) seedV[i] = W.rng()\n\n  return {\n    step(dt) {\n      const o = W.o\n      const decay = Math.exp(-dt * 2.4 * Math.max(0.2, o.speed))\n      for (let i = 0; i < n; i++) energy[i] *= decay\n      if (W.presence > 0.01) {\n        const R = o.radius\n        const c0 = Math.max(0, Math.floor((W.px - R - x0) / cs))\n        const c1 = Math.min(cols - 1, Math.ceil((W.px + R - x0) / cs))\n        const r0 = Math.max(0, Math.floor((W.py - R - y0) / cs))\n        const r1 = Math.min(rows - 1, Math.ceil((W.py + R - y0) / cs))\n        for (let j = r0; j <= r1; j++) {\n          for (let i = c0; i <= c1; i++) {\n            const cx = x0 + (i + 0.5) * cs\n            const cy = y0 + (j + 0.5) * cs\n            // Ragged edge: jitter the falloff per cell\n            const f = influence(W, cx, cy) * (0.55 + W.rng() * 0.45) * Math.min(1.4, o.strength)\n            const idx = j * cols + i\n            if (f > energy[idx]) energy[idx] = Math.min(1, f)\n          }\n        }\n      }\n      if (W.rippling) {\n        for (let j = 0; j < rows; j++) {\n          for (let i = 0; i < cols; i++) {\n            const r = rippleAt(W, x0 + (i + 0.5) * cs, y0 + (j + 0.5) * cs)\n            const idx = j * cols + i\n            if (r > energy[idx]) energy[idx] = Math.min(1, r * o.strength)\n          }\n        }\n      }\n      const t = W.t\n      for (let i = 0; i < n; i++) {\n        const e = energy[i]\n        if (e > 0.03) {\n          // Lit cells flicker slightly as they fade\n          const flick = W.rng() < 0.06 ? 0.6 : 1\n          pi[i] = paletteIndex(e * flick, 0)\n        } else if (seedV[i] > 0.86) {\n          const tw = (Math.sin(t * 0.9 + seedV[i] * 60) + 1) * 0.5\n          pi[i] = Math.min(FADE - 1, Math.floor(tw * FADE))\n        } else {\n          pi[i] = 255\n        }\n      }\n    },\n    draw() {\n      const ctx = W.ctx\n      resetCounts(W)\n      for (let i = 0; i < n; i++) if (pi[i] !== 255) W.counts[pi[i]]++\n      const gap = Math.min(cs * 0.5, Math.max(0, W.o.size))\n      const side = cs - gap\n      for (let p = 0; p < NB; p++) {\n        if (!W.counts[p]) continue\n        ctx.fillStyle = W.pal[p]\n        ctx.beginPath()\n        for (let j = 0; j < rows; j++) {\n          for (let i = 0; i < cols; i++) {\n            if (pi[j * cols + i] !== p) continue\n            ctx.rect(x0 + i * cs + gap / 2, y0 + j * cs + gap / 2, side, side)\n          }\n        }\n        ctx.fill()\n      }\n    },\n  }\n}\n\nconst SCENES: Record<HeroBackgroundVariant, SceneFactory> = { dots, grid, particles, waves, flow, pixels }\n\n// ---------------------------------------------------------------- color\n\ninterface Rgba {\n  r: number\n  g: number\n  b: number\n  a: number\n}\n\nfunction resolveColor(probe: HTMLElement, value: string, scratch: CanvasRenderingContext2D | null): Rgba {\n  probe.style.color = \"\"\n  probe.style.color = value\n  if (!probe.style.color) probe.style.color = \"currentColor\"\n  const computed = getComputedStyle(probe).color\n  if (!scratch) return { r: 128, g: 128, b: 128, a: 1 }\n  scratch.clearRect(0, 0, 1, 1)\n  scratch.fillStyle = \"#808080\"\n  scratch.fillStyle = computed\n  scratch.fillRect(0, 0, 1, 1)\n  const d = scratch.getImageData(0, 0, 1, 1).data\n  return { r: d[0], g: d[1], b: d[2], a: d[3] / 255 }\n}\n\nconst rgba = (r: number, g: number, b: number, a: number) =>\n  `rgba(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)}, ${Math.max(0, Math.min(1, a)).toFixed(3)})`\n\nfunction buildPalette(W: World, c: Rgba, a: Rgba) {\n  const op = Math.max(0, Math.min(1, W.o.opacity))\n  for (let i = 0; i < FADE; i++) W.pal[i] = rgba(c.r, c.g, c.b, c.a * op * ((i + 1) / FADE))\n  for (let k = 0; k < LEVELS; k++) {\n    const t = k / (LEVELS - 1)\n    const alpha = (c.a + (a.a - c.a) * t) * (op + (1 - op) * t * 0.9)\n    W.pal[BASE + k] = rgba(c.r + (a.r - c.r) * t, c.g + (a.g - c.g) * t, c.b + (a.b - c.b) * t, alpha)\n    W.hl[k] = rgba(a.r, a.g, a.b, a.a * t * 0.14)\n  }\n}\n\n// Alpha mask for edge fading, not a color gradient\nconst MASKS: Record<HeroBackgroundMask, CSSProperties | undefined> = {\n  none: undefined,\n  radial: {\n    maskImage: \"radial-gradient(ellipse 70% 65% at 50% 50%, #000 25%, transparent 100%)\",\n    WebkitMaskImage: \"radial-gradient(ellipse 70% 65% at 50% 50%, #000 25%, transparent 100%)\",\n  },\n  \"fade-bottom\": {\n    maskImage: \"linear-gradient(to bottom, #000 35%, transparent 100%)\",\n    WebkitMaskImage: \"linear-gradient(to bottom, #000 35%, transparent 100%)\",\n  },\n  \"fade-edges\": {\n    maskImage:\n      \"linear-gradient(to right, transparent, #000 18%, #000 82%, transparent), linear-gradient(to bottom, transparent, #000 18%, #000 82%, transparent)\",\n    WebkitMaskImage:\n      \"linear-gradient(to right, transparent, #000 18%, #000 82%, transparent), linear-gradient(to bottom, transparent, #000 18%, #000 82%, transparent)\",\n    maskComposite: \"intersect\",\n    WebkitMaskComposite: \"source-in\",\n  },\n}\n\n// ---------------------------------------------------------------- component\n\nexport function HeroBackground({\n  variant = \"dots\",\n  color = \"currentColor\",\n  accent = \"#ff4d12\",\n  background = \"transparent\",\n  density = 1,\n  size = 1.5,\n  speed = 1,\n  interaction = \"repel\",\n  radius = 160,\n  strength = 1,\n  opacity = 0.35,\n  mask = \"none\",\n  clickRipple = true,\n  interactive = true,\n  seed = 1,\n  fps = 60,\n  children,\n  className,\n}: HeroBackgroundProps) {\n  const rootRef = useRef<HTMLDivElement>(null)\n  const canvasRef = useRef<HTMLCanvasElement>(null)\n  const probeRef = useRef<HTMLSpanElement>(null)\n  const liveRef = useRef<Live | null>(null)\n  const versionRef = useRef(0)\n  const redrawRef = useRef<(() => void) | null>(null)\n  const reduced = useReducedMotion()\n\n  // Live options: applied on the next frame without rebuilding the scene\n  useEffect(() => {\n    liveRef.current = {\n      color,\n      accent,\n      size: Math.max(0, size),\n      speed: Math.max(0, speed),\n      sign: interaction === \"repel\" ? 1 : interaction === \"attract\" ? -1 : 0,\n      radius: Math.max(1, radius),\n      strength: Math.max(0, strength),\n      opacity,\n      clickRipple,\n      fps: Math.max(1, fps),\n    }\n    versionRef.current++\n    redrawRef.current?.()\n  }, [color, accent, size, speed, interaction, radius, strength, opacity, clickRipple, fps])\n\n  useEffect(() => {\n    const root = rootRef.current\n    const canvas = canvasRef.current\n    const probe = probeRef.current\n    const ctx = canvas?.getContext(\"2d\")\n    const live = liveRef.current\n    if (!root || !canvas || !probe || !ctx || !live) return\n\n    const scratchCanvas = document.createElement(\"canvas\")\n    scratchCanvas.width = scratchCanvas.height = 1\n    const scratch = scratchCanvas.getContext(\"2d\", { willReadFrequently: true })\n    const rip = new Float32Array(RIPPLES * 3)\n    for (let i = 0; i < RIPPLES; i++) rip[i * 3 + 2] = -1\n\n    const W: World = {\n      ctx,\n      w: 0,\n      h: 0,\n      t: 0,\n      px: 0,\n      py: 0,\n      pvx: 0,\n      pvy: 0,\n      presence: 0,\n      rip,\n      rippling: false,\n      rx: 0,\n      ry: 0,\n      o: live,\n      pal: new Array<string>(NB).fill(\"transparent\"),\n      hl: new Array<string>(LEVELS).fill(\"transparent\"),\n      rng: mulberry32(seed),\n      noise: makeNoise(mulberry32(seed ^ 0x9e3779b9)),\n      density: Math.max(0.1, density),\n      counts: new Int32Array(NB),\n    }\n\n    let scene: Scene | null = null\n    let raf = 0\n    let last = 0\n    let seenVersion = -1\n    let colorsDirty = true\n    let onScreen = true\n    let tx = 0\n    let ty = 0\n    let inside = false\n    let nextRipple = 0\n\n    const refreshColors = () => {\n      if (liveRef.current) W.o = liveRef.current\n      if (!colorsDirty && seenVersion === versionRef.current) return\n      seenVersion = versionRef.current\n      colorsDirty = false\n      buildPalette(W, resolveColor(probe, W.o.color, scratch), resolveColor(probe, W.o.accent, scratch))\n    }\n\n    const render = () => {\n      ctx.clearRect(0, 0, W.w, W.h)\n      scene?.draw()\n    }\n\n    const advance = (dt: number) => {\n      refreshColors()\n      const follow = 1 - Math.exp(-dt * 16)\n      if (inside && W.presence < 0.02) {\n        W.px = tx\n        W.py = ty\n      }\n      const lpx = W.px\n      const lpy = W.py\n      W.px += (tx - W.px) * follow\n      W.py += (ty - W.py) * follow\n      const vEase = 1 - Math.exp(-dt * 10)\n      W.pvx += ((W.px - lpx) / dt - W.pvx) * vEase\n      W.pvy += ((W.py - lpy) / dt - W.pvy) * vEase\n      W.presence += ((inside ? 1 : 0) - W.presence) * (1 - Math.exp(-dt * (inside ? 8 : 4)))\n      W.rippling = false\n      for (let i = 0; i < RIPPLES; i++) {\n        if (rip[i * 3 + 2] < 0) continue\n        rip[i * 3 + 2] += dt\n        if (rip[i * 3 + 2] > RIPPLE_LIFE) rip[i * 3 + 2] = -1\n        else W.rippling = true\n      }\n      W.t += dt * W.o.speed\n      scene?.step(dt)\n    }\n\n    const shouldRun = () => !reduced && onScreen && !document.hidden && W.w > 0 && W.h > 0\n\n    const frame = (now: number) => {\n      raf = 0\n      if (!shouldRun()) return\n      raf = requestAnimationFrame(frame)\n      const minGap = 1000 / W.o.fps\n      if (last && now - last < minGap - 1.5) return\n      const dt = last ? Math.min(Math.max((now - last) / 1000, 0.001), 0.05) : 1 / 60\n      last = now\n      advance(dt)\n      render()\n    }\n\n    const start = () => {\n      if (raf || !shouldRun()) return\n      last = 0\n      raf = requestAnimationFrame(frame)\n    }\n\n    /** Reduced motion: settle and draw a single frame. */\n    const renderStatic = () => {\n      if (!scene) return\n      refreshColors()\n      const steps = variant === \"flow\" ? TRAIL + 4 : 1\n      for (let i = 0; i < steps; i++) {\n        W.t += 1 / 60\n        scene.step(1 / 60)\n      }\n      render()\n    }\n\n    const resize = () => {\n      const rect = root.getBoundingClientRect()\n      const w = Math.round(rect.width)\n      const h = Math.round(rect.height)\n      if (w === W.w && h === W.h && scene) return\n      const dpr = Math.min(window.devicePixelRatio || 1, 2)\n      canvas.width = Math.max(1, Math.round(w * dpr))\n      canvas.height = Math.max(1, Math.round(h * dpr))\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0)\n      W.w = w\n      W.h = h\n      W.t = 0\n      W.rng = mulberry32(seed)\n      colorsDirty = true\n      scene = w > 0 && h > 0 ? SCENES[variant](W) : null\n      if (reduced) renderStatic()\n      else {\n        refreshColors()\n        advance(1 / 60)\n        render()\n        start()\n      }\n    }\n\n    redrawRef.current = () => {\n      if (reduced) {\n        refreshColors()\n        render()\n      }\n    }\n\n    const onMove = (e: PointerEvent) => {\n      const rect = root.getBoundingClientRect()\n      tx = e.clientX - rect.left\n      ty = e.clientY - rect.top\n      inside = true\n    }\n    const onLeave = () => {\n      inside = false\n    }\n    const onDown = (e: PointerEvent) => {\n      onMove(e)\n      if (!W.o.clickRipple) return\n      rip[nextRipple * 3] = tx\n      rip[nextRipple * 3 + 1] = ty\n      rip[nextRipple * 3 + 2] = 0\n      nextRipple = (nextRipple + 1) % RIPPLES\n    }\n\n    const markDirty = () => {\n      colorsDirty = true\n      if (reduced) {\n        refreshColors()\n        render()\n      }\n    }\n\n    const ro = new ResizeObserver(resize)\n    ro.observe(root)\n    const io = new IntersectionObserver((entries) => {\n      onScreen = entries[entries.length - 1]?.isIntersecting ?? true\n      if (onScreen) start()\n    })\n    io.observe(root)\n    const onVisibility = () => {\n      if (!document.hidden) start()\n    }\n    document.addEventListener(\"visibilitychange\", onVisibility)\n    // Theme switches change the resolved currentColor without a resize\n    const mo = new MutationObserver(markDirty)\n    mo.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\", \"style\", \"data-theme\"] })\n    const scheme = window.matchMedia(\"(prefers-color-scheme: dark)\")\n    scheme.addEventListener(\"change\", markDirty)\n\n    const listen = interactive && !reduced\n    if (listen) {\n      root.addEventListener(\"pointermove\", onMove)\n      root.addEventListener(\"pointerdown\", onDown)\n      root.addEventListener(\"pointerleave\", onLeave)\n      root.addEventListener(\"pointercancel\", onLeave)\n    }\n    resize()\n\n    return () => {\n      cancelAnimationFrame(raf)\n      raf = 0\n      redrawRef.current = null\n      ro.disconnect()\n      io.disconnect()\n      mo.disconnect()\n      scheme.removeEventListener(\"change\", markDirty)\n      document.removeEventListener(\"visibilitychange\", onVisibility)\n      if (listen) {\n        root.removeEventListener(\"pointermove\", onMove)\n        root.removeEventListener(\"pointerdown\", onDown)\n        root.removeEventListener(\"pointerleave\", onLeave)\n        root.removeEventListener(\"pointercancel\", onLeave)\n      }\n    }\n  }, [variant, density, seed, interactive, reduced])\n\n  return (\n    <div\n      ref={rootRef}\n      data-slot=\"hero-background\"\n      className={cn(\"relative isolate overflow-hidden\", className)}\n      style={{ background }}\n    >\n      <div aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0 -z-10\" style={MASKS[mask]}>\n        <canvas ref={canvasRef} className=\"block size-full\" />\n        <span ref={probeRef} hidden />\n      </div>\n      {children}\n    </div>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "docs": "Docs, live playground and every prop: https://trytweenly.vercel.app/docs/components/hero-background",
  "type": "registry:component"
}