{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hero-ascii-eclipse",
  "title": "Hero ASCII Eclipse",
  "description": "A full-bleed ASCII hero built around an occlusion event: a dark glyph disc transits a bright sun disc over a fixed starfield, the pointer dragging the transit, and a corona blooms at the rim only as the two centers nearly align, the way a real solar corona is only visible near totality.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/loud/hero-ascii-eclipse/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport type { ReactNode } from \"react\";\n\n// ---------------------------------------------------------------------------\n// Totality — a full-bleed ASCII hero built around an OCCLUSION event: a dark\n// disc (the moon) transits a bright glyph disc (the sun) sitting over a\n// sparse fixed starfield. This is a compositing problem, not a field-sample\n// or particle problem — per cell, in strict paint order: inside the moon's\n// radius is always blank (it occludes everything beneath it, sun or star,\n// unconditionally); inside the sun's radius but outside the moon draws the\n// photosphere (density ramp, brighter toward its own center); a ring band\n// just outside the sun's radius draws the CORONA — glyphs whose density\n// carries an angular \"streamer\" flicker (a per-angle sine, not noise, so the\n// flare pattern is smooth and repeatable) — but the corona's overall\n// intensity is a single scalar driven by how closely the moon's center\n// currently sits over the sun's: it is near zero during an ordinary partial\n// transit and only blooms as the two centers nearly coincide (totality),\n// which is the actual astronomy the effect is named for. Elsewhere, the\n// fixed starfield (identical technique to hero-ascii-terrain's sky) shows\n// through. The pointer DRAGS the moon across the sun (eased toward the raw\n// pointer position); at rest, a slow idle drift carries it back and forth on\n// its own so the transit is never a dead frame.\n// ---------------------------------------------------------------------------\n\nconst RAMP = \" .:-=+*#%@\";\nconst STAR_CHARS = [1, 2, 3];\nconst CURSOR_EASE = 0.08;\nconst IDLE_PERIOD = 9; // s for one full idle sweep across the sun\nconst DT_MAX = 0.05;\n\nfunction mulberry32(a: number) {\n  return () => {\n    a |= 0;\n    a = (a + 0x6d2b79f5) | 0;\n    let t = Math.imul(a ^ (a >>> 15), 1 | a);\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nexport interface TotalityProps {\n  /** grid cell size in px */\n  cellSize?: number;\n  /** headline / CTA rendered over the field */\n  children?: ReactNode;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nexport function Totality({\n  cellSize = 13,\n  children,\n  className = \"\",\n}: TotalityProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n\n  useEffect(() => {\n    const root = rootRef.current;\n    const canvas = canvasRef.current;\n    if (!root || !canvas) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    const reduced = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\"\n    ).matches;\n\n    let fg = \"currentColor\";\n    let muted = \"currentColor\";\n    let accent = \"currentColor\";\n    let cellW = cellSize;\n    let cellH = cellSize;\n    let cols = 0;\n    let rows = 0;\n    let dpr = 1;\n    let sized = false;\n    let ready = false;\n    let disposed = false;\n\n    let sunPx = 0;\n    let sunPy = 0;\n    let sunR = 0;\n    let moonR = 0;\n    let coronaW = 0;\n\n    let starCol = new Float32Array(0);\n    let starRow = new Float32Array(0);\n    let starChar = new Uint8Array(0);\n    let starPhase = new Float32Array(0);\n    let starAlpha = new Float32Array(0);\n    let starCount = 0;\n\n    const readTokens = () => {\n      fg = getComputedStyle(canvas).color;\n      const cs = getComputedStyle(document.documentElement);\n      muted = cs.getPropertyValue(\"--ns-muted\").trim() || fg;\n      accent = cs.getPropertyValue(\"--ns-accent\").trim() || fg;\n    };\n\n    const measureCell = (fontFamily: string) => {\n      const off = document.createElement(\"canvas\");\n      const octx = off.getContext(\"2d\");\n      if (!octx) return;\n      octx.font = `${cellSize}px ${fontFamily}`;\n      cellW = Math.max(4, octx.measureText(\"MMMMMMMMMM\").width / 10);\n      cellH = cellSize;\n    };\n\n    const buildStars = () => {\n      const w = cols * cellW;\n      const h = rows * cellH;\n      const rand = mulberry32(0x0ec11950 ^ (cols * 71 + rows));\n      starCount = Math.min(260, Math.max(0, Math.floor(cols * rows * 0.045)));\n      starCol = new Float32Array(starCount);\n      starRow = new Float32Array(starCount);\n      starChar = new Uint8Array(starCount);\n      starPhase = new Float32Array(starCount);\n      starAlpha = new Float32Array(starCount);\n      for (let i = 0; i < starCount; i++) {\n        starCol[i] = rand() * w;\n        starRow[i] = rand() * h;\n        starChar[i] = STAR_CHARS[Math.floor(rand() * STAR_CHARS.length)]!;\n        starPhase[i] = rand() * Math.PI * 2;\n        starAlpha[i] = 0.25 + rand() * 0.45;\n      }\n    };\n\n    const resize = () => {\n      const { width, height } = canvas.getBoundingClientRect();\n      if (width < 2 || height < 2) {\n        sized = false;\n        return;\n      }\n      dpr = Math.min(window.devicePixelRatio || 1, 2);\n      canvas.width = Math.max(1, Math.round(width * dpr));\n      canvas.height = Math.max(1, Math.round(height * dpr));\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      const fontFamily = getComputedStyle(canvas).fontFamily;\n      measureCell(fontFamily);\n      ctx.font = `${cellSize}px ${fontFamily}`;\n      ctx.textAlign = \"center\";\n      ctx.textBaseline = \"middle\";\n\n      cols = Math.max(8, Math.ceil(width / cellW));\n      rows = Math.max(8, Math.ceil(height / cellH));\n\n      sunPx = cols * cellW * 0.5;\n      sunPy = rows * cellH * 0.42;\n      sunR = Math.min(cols * cellW, rows * cellH) * 0.15;\n      moonR = sunR * 1.04;\n      coronaW = sunR * 0.65;\n\n      buildStars();\n      sized = true;\n    };\n\n    let resizeTimer: ReturnType<typeof setTimeout> | null = null;\n    const onResize = () => {\n      if (resizeTimer) clearTimeout(resizeTimer);\n      resizeTimer = setTimeout(() => {\n        resizeTimer = null;\n        resize();\n        if (reduced) draw(0, sunPx - moonR * 2.4, sunPy);\n      }, 150);\n    };\n\n    const draw = (t: number, moonPx: number, moonPy: number) => {\n      if (!sized) return;\n      const w = cols * cellW;\n      const h = rows * cellH;\n      ctx.clearRect(0, 0, w, h);\n\n      const centerDist = Math.hypot(moonPx - sunPx, moonPy - sunPy);\n      const coronaIntensity = Math.pow(\n        Math.max(0, 1 - centerDist / (sunR * 0.85)),\n        2\n      );\n\n      // -- starfield: sparse breathing background, drawn first ---------------\n      ctx.fillStyle = muted;\n      for (let i = 0; i < starCount; i++) {\n        const dm = Math.hypot(starCol[i]! - moonPx, starRow[i]! - moonPy);\n        if (dm <= moonR) continue; // occluded by the moon\n        const twinkle = 0.6 + 0.4 * Math.sin(t * 0.6 + starPhase[i]!);\n        ctx.globalAlpha = starAlpha[i]! * twinkle;\n        ctx.fillText(RAMP[starChar[i]!]!, starCol[i]!, starRow[i]!);\n      }\n\n      // -- sun + corona + moon occlusion, one pass over a coarse cell grid ---\n      const outerR = sunR + coronaW;\n      for (let gy = 0; gy < rows; gy++) {\n        const py = gy * cellH + cellH / 2;\n        for (let gx = 0; gx < cols; gx++) {\n          const px = gx * cellW + cellW / 2;\n\n          const dMoon = Math.hypot(px - moonPx, py - moonPy);\n          if (dMoon <= moonR) continue; // the moon occludes everything, always\n\n          const dSun = Math.hypot(px - sunPx, py - sunPy);\n          if (dSun <= sunR) {\n            const lum = 1 - (dSun / sunR) * 0.55;\n            const ci = Math.max(1, Math.floor(lum * (RAMP.length - 1)));\n            ctx.fillStyle = fg;\n            ctx.globalAlpha = 0.55 + lum * 0.45;\n            ctx.fillText(RAMP[ci]!, px, py);\n            continue;\n          }\n\n          if (dSun <= outerR && coronaIntensity > 0.02) {\n            const radial = 1 - (dSun - sunR) / coronaW;\n            const angle = Math.atan2(py - sunPy, px - sunPx);\n            const streamer = 0.55 + 0.45 * Math.sin(angle * 7 + t * 1.4);\n            const lum = radial * streamer * coronaIntensity;\n            if (lum > 0.05) {\n              const ci = Math.max(1, Math.floor(lum * (RAMP.length - 1)));\n              ctx.fillStyle = accent;\n              ctx.globalAlpha = Math.min(1, lum);\n              ctx.fillText(RAMP[ci]!, px, py);\n            }\n          }\n        }\n      }\n      ctx.globalAlpha = 1;\n    };\n\n    // -- hot-path state -------------------------------------------------------\n    let raf = 0;\n    let last = 0;\n    let t = 0;\n    const cursor = { has: false, tx: 0, ty: 0, x: 0, y: 0 };\n\n    const loop = (now: number) => {\n      const dt = last ? Math.min(DT_MAX, (now - last) / 1000) : 1 / 60;\n      last = now;\n      t += dt;\n\n      let moonPx: number;\n      let moonPy: number;\n      if (cursor.has) {\n        cursor.x += (cursor.tx - cursor.x) * CURSOR_EASE;\n        cursor.y += (cursor.ty - cursor.y) * CURSOR_EASE;\n        moonPx = cursor.x;\n        moonPy = cursor.y;\n      } else {\n        // idle transit: a slow sweep back and forth through the sun\n        const phase = (t / IDLE_PERIOD) % 1;\n        const sweep = Math.sin(phase * Math.PI * 2);\n        moonPx = sunPx + sweep * moonR * 2.6;\n        moonPy = sunPy + Math.sin(phase * Math.PI * 4) * moonR * 0.35;\n        cursor.x = moonPx;\n        cursor.y = moonPy;\n      }\n\n      draw(t, moonPx, moonPy);\n      if (!document.hidden) raf = requestAnimationFrame(loop);\n    };\n\n    const onPointerMove = (e: PointerEvent) => {\n      const rect = root.getBoundingClientRect();\n      cursor.tx = e.clientX - rect.left;\n      cursor.ty = e.clientY - rect.top;\n      cursor.has = true;\n    };\n    const onPointerLeave = () => {\n      cursor.has = false;\n    };\n\n    const onVis = () => {\n      if (!document.hidden && !reduced && ready) {\n        last = 0;\n        raf = requestAnimationFrame(loop);\n      }\n    };\n    const mo = new MutationObserver(() => {\n      readTokens();\n      if (reduced) draw(0, sunPx - moonR * 2.4, sunPy);\n    });\n    mo.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    document.fonts.ready.then(() => {\n      if (disposed) return;\n      readTokens();\n      resize();\n      ready = true;\n      if (reduced) {\n        // static frame: moon well clear of the sun, an ordinary partial\n        // transit moment, corona at rest (near zero)\n        draw(0, sunPx - moonR * 2.4, sunPy);\n      } else {\n        raf = requestAnimationFrame(loop);\n      }\n    });\n\n    window.addEventListener(\"resize\", onResize);\n    if (!reduced) {\n      root.addEventListener(\"pointermove\", onPointerMove);\n      root.addEventListener(\"pointerleave\", onPointerLeave);\n    }\n    document.addEventListener(\"visibilitychange\", onVis);\n\n    return () => {\n      disposed = true;\n      cancelAnimationFrame(raf);\n      if (resizeTimer) clearTimeout(resizeTimer);\n      mo.disconnect();\n      window.removeEventListener(\"resize\", onResize);\n      root.removeEventListener(\"pointermove\", onPointerMove);\n      root.removeEventListener(\"pointerleave\", onPointerLeave);\n      document.removeEventListener(\"visibilitychange\", onVis);\n    };\n  }, [cellSize]);\n\n  return (\n    <div\n      ref={rootRef}\n      className={`relative isolate w-full overflow-hidden bg-background font-mono ${\n        /\\bmin-h-/.test(className) ? \"\" : \"min-h-screen\"\n      } ${className}`}\n    >\n      <canvas\n        ref={canvasRef}\n        aria-hidden\n        className=\"absolute inset-0 block h-full w-full text-foreground\"\n      />\n      {children ? (\n        <div className=\"relative z-10 flex h-full w-full flex-col items-start justify-end gap-4 p-8 sm:p-14\">\n          {children}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/hero-ascii-eclipse.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "loud",
    "tags": [
      "ascii",
      "hero",
      "background",
      "canvas",
      "cursor",
      "eclipse",
      "occlusion"
    ],
    "instruction": "Build a full-bleed Canvas 2D hero: a fixed sun disc (radius ~15% of the shorter canvas dimension, centered slightly above screen-center) sits over a sparse fixed breathing starfield (identical technique to hero-ascii-terrain's sky: seeded via mulberry32, alpha twinkling on a per-star sine phase), and a moon disc (radius ~1.04x the sun's, so it can fully cover it) transits across it. COMPOSITING, per cell, in strict occlusion order: if the cell is within the moon's radius, it is ALWAYS blank, unconditionally occluding sun or star beneath it — there is no partial-transparency moon. Otherwise, if within the sun's radius, draw the photosphere: the shared ' .:-=+*#%@' ramp indexed by a radial falloff (brighter toward the sun's own center), in the --foreground token. Otherwise, if within a corona ring band just outside the sun's radius (width ~65% of the sun's radius) AND the scalar 'corona intensity' is above a small threshold, draw a corona glyph whose density is the product of a radial falloff and an angular 'streamer' term — sin(angle * 7 + t * 1.4), a smooth per-angle flicker, not noise — scaled by that intensity, rendered in the --ns-accent token (the one place in this hero color leaves pure ink, deliberately, since a corona is the visual payoff). CORONA INTENSITY is a single scalar, pow(max(0, 1 - centerDistance / (sunRadius * 0.85)), 2): near zero for an ordinary partial transit and blooming only as the moon's center nearly coincides with the sun's — real eclipse astronomy, not a cosmetic pulse. Otherwise the cell falls through to the starfield (occluded by the moon check above already handled) or stays blank. The moon's position is the pointer's, inside the container, eased 0.08/frame toward the raw position; with no active pointer, an idle sweep (9s period, sine-driven horizontal traverse of ~2.6 moon-radii either side of the sun plus a small vertical bob) keeps the transit alive on its own. Direct-DOM rAF, zero React state on the hot path; grid buffers/typed arrays sized once per resize (cols/rows via Math.ceil, not floor). Ink is read via getComputedStyle(canvas).color plus the --ns-muted and --ns-accent custom properties at mount, re-derived on a documentElement class MutationObserver. Mono cell measured via an offscreen canvas's measureText. prefers-reduced-motion renders exactly one static frame with the moon parked clear of the sun (an ordinary partial-transit moment, corona at rest) and skips the rAF loop and pointer listeners entirely. Loop pauses on document.hidden, resumes on visibilitychange. Optional children render over the field, bottom-left anchored with padding. Props: cellSize (grid cell px, default 13), children, className.",
    "rank": 9
  },
  "type": "registry:ui"
}