{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hero-ascii-shock-diamonds",
  "title": "Hero ASCII Shock Diamonds",
  "description": "A full-bleed ASCII hero of a supersonic jet plume: a repeating train of oblique shocks reflecting off the jet boundary draws the classic X-crossed Mach diamonds as thin bright lines in an otherwise empty exhaust, with the pointer acting as the throttle that stretches and compresses the shock-cell spacing.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/loud/hero-ascii-shock-diamonds/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport type { ReactNode } from \"react\";\n\n// ---------------------------------------------------------------------------\n// ShockTrain — a full-bleed ASCII hero of a supersonic exhaust plume. This is\n// gas dynamics, not a pattern: an underexpanded jet leaving a nozzle turns\n// through a fan, over-expands, and is turned back by an OBLIQUE SHOCK that\n// leaves the nozzle lip at the Mach angle mu = asin(1/M). That shock reflects\n// specularly off the free jet boundary, crosses its mirror image from the\n// opposite lip, and the whole thing repeats — a shock CELL — every\n// L = 1.30 * D * sqrt(M^2 - 1) (Prandtl-Pack). Two travelling families is all\n// it takes: p1 = a + b and p2 = a - b, where a is downstream distance in cells\n// and b is transverse distance in cells; a cell is ON a shock when either\n// phase lands near an integer. Where BOTH land near an integer the two\n// families cross, and that crossing is the diamond node — it is inked 1.9x\n// harder, which is the whole reason the frame reads as a string of bright X's\n// on the centreline rather than a diagonal hatch. The design Mach number is\n// LOW (1.5) on purpose: the Mach angle is then 42 degrees, so the two families\n// cross at close to a right angle and the arms read as chevrons instead of\n// merging into horizontal streaks, and the shorter cell length lets D grow to\n// 0.28*H so the train is a tall band rather than a thin line. Everything else\n// is blank: the shocks are hard-masked to the plume's own waisted envelope,\n// and the plume's interior between shocks is empty. The plume rides HIGH in\n// the frame — axis at 0.26*H — because the hero copy is anchored bottom-left\n// and the two must not overlap. The pointer is the THROTTLE — its\n// x position sets the nozzle pressure ratio, M is re-solved from it\n// isentropically, and since L scales with sqrt(M^2 - 1) the diamonds visibly\n// stretch apart to the right and compress to the left. Pointer y vectors the\n// jet axis. Both ease back to the design plume when the pointer leaves.\n// ---------------------------------------------------------------------------\n\nconst RAMP = \" .:-=+*#%@\";\nconst ALPHA_BUCKETS = 6;\nconst SHOCK_TOL = 0.035; // phase half-width of a shock, in cell lengths\nconst SHOCK_POW = 1.6; // line profile sharpness\nconst NODE_GAIN = 1.9; // crossing multiplier — makes the X's the picture\nconst INK_DECAY = 6.0; // shock strength e-folding, in cell lengths\nconst ENV_DECAY = 4.5; // plume envelope e-folding, in cell lengths\nconst PLUME_END = 6.0; // stop evaluating past this many cells\nconst THROTTLE_TAU = 0.6; // s — pointer easing time constant\nconst PULSE_A = 0.018; // resting breath in cell spacing\nconst PULSE_B = 0.008;\nconst CELLS_ACROSS = 3.2; // design: shock cells that fit the frame at rest\nconst AXIS_FRAC = 0.26; // jet axis height — copy owns the bottom of the frame\nconst D_MAX_FRAC = 0.28; // hard cap on nozzle diameter, as a fraction of H\nconst VECTOR_FRAC = 0.04; // thrust-vector travel, as a fraction of H\nconst NPR_LO = 0.66; // throttle window, as a multiple of the design NPR\nconst NPR_HI = 2.13;\nconst DT_MAX = 0.05;\n\n/** isentropic (gamma 1.4) Mach number from a nozzle pressure ratio */\nfunction machFromNpr(npr: number): number {\n  const m2 = 5 * (Math.pow(Math.max(1.0001, npr), 0.2857) - 1);\n  return Math.max(1.05, Math.sqrt(Math.max(0.1025, m2)));\n}\n\n/** nozzle pressure ratio that produces this design Mach number */\nfunction nprFromMach(m: number): number {\n  return Math.pow(1 + 0.2 * m * m, 3.5);\n}\n\n/** distance from p to the nearest integer, in [0, 0.5] */\nfunction phaseDist(p: number): number {\n  const f = p - Math.floor(p);\n  return f < 0.5 ? f : 1 - f;\n}\n\nexport interface ShockTrainProps {\n  /** grid cell size in px */\n  cellSize?: number;\n  /** design Mach number of the plume at rest */\n  mach?: number;\n  /** headline / CTA rendered over the plume */\n  children?: ReactNode;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nexport function ShockTrain({\n  cellSize = 12,\n  mach = 1.5,\n  children,\n  className = \"\",\n}: ShockTrainProps) {\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    const machDesign = Math.min(4.5, Math.max(1.2, mach));\n    const nprDesign = nprFromMach(machDesign);\n    const nprMin = nprDesign * NPR_LO;\n    const nprMax = nprDesign * NPR_HI;\n\n    let fg = \"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 nozzleX = 0; // x0 — nozzle exit plane\n    let axisY = 0; // yc at rest — the undeflected jet axis\n    let nozzleD = 0; // D — nozzle exit diameter\n    let vectorMax = 0; // px of thrust-vector travel\n\n    let charBuf = new Uint8Array(0);\n    const bucketLists: number[][] = Array.from(\n      { length: ALPHA_BUCKETS },\n      () => []\n    );\n\n    const readTokens = () => {\n      fg = getComputedStyle(canvas).color;\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 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      const w = cols * cellW;\n      const h = rows * cellH;\n      nozzleX = w * 0.1;\n      // the jet axis is snapped to a ROW CENTRE: the on-axis diamond node sits\n      // exactly at y = yc, and if that lands between two rows the brightest\n      // cell in the whole picture is never sampled and the train degrades into\n      // a chevron hatch. Snapping is what keeps the X's on the centreline.\n      axisY =\n        Math.round((h * AXIS_FRAC - cellH / 2) / cellH) * cellH + cellH / 2;\n      vectorMax = h * VECTOR_FRAC;\n      // D is the one free geometric parameter and it sets how TALL the train\n      // is: the shock band is about 1.5*D high, so D is pushed as large as the\n      // frame allows — up to 0.28*H — and the cell count is what gives way.\n      // Only ~3.5 cells lay across the frame (three crossings and the lip),\n      // which is the point: three legible diamonds beat six horizontal dashes.\n      // Pinned to machDesign, never the live Mach — D is the hardware, and it\n      // is precisely because D is fixed that a throttle change moves L.\n      const kDesign = 1.3 * Math.sqrt(machDesign * machDesign - 1);\n      nozzleD = Math.min(\n        h * D_MAX_FRAC,\n        ((w - nozzleX) * 0.98) / (CELLS_ACROSS * kDesign)\n      );\n      nozzleD = Math.max(nozzleD, cellH * 8);\n\n      charBuf = new Uint8Array(cols * rows);\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, machDesign, 0);\n      }, 150);\n    };\n\n    const draw = (t: number, machEff: number, vectorY: number) => {\n      if (!sized) return;\n      const w = cols * cellW;\n      const h = rows * cellH;\n      ctx.clearRect(0, 0, w, h);\n      for (let b = 0; b < ALPHA_BUCKETS; b++) bucketLists[b]!.length = 0;\n\n      const M = Math.max(1.05, machEff);\n      // Prandtl-Pack shock-cell length, and the Mach angle the lip shock\n      // leaves at. Both move together when the throttle moves.\n      const L = 1.3 * nozzleD * Math.sqrt(M * M - 1);\n      const tanMu = Math.tan(Math.asin(1 / M));\n      const invBand = 1 / (L * tanMu);\n      // thrust vectoring moves the axis in whole rows, so the node stays on a\n      // row centre wherever it is aimed\n      const yc = axisY + Math.round(vectorY / cellH) * cellH;\n      const rMax = 0.5 * nozzleD * 1.55;\n      const envHalf = cellH * 0.55;\n      const tau = Math.PI * 2;\n\n      // A shock is 0.035 cell-lengths of phase wide, which on this grid is\n      // thinner than one row — sampled naively the line falls between cells and\n      // breaks into dashes. Dilate the hit test to at least one cell while\n      // keeping the physical 0.035 profile: everything inside the dilation is\n      // full-strength line, and the original profile fades over the last\n      // 0.035. Conservative rasterization, not a fattened shock.\n      const gradMag = Math.hypot(1 / L, invBand);\n      const tol = Math.max(SHOCK_TOL, 0.62 * cellH * gradMag);\n      const dilate = tol - SHOCK_TOL;\n      const profile = (d: number) => {\n        const de = d <= dilate ? 0 : d - dilate;\n        return Math.pow(Math.max(0, 1 - de / SHOCK_TOL), SHOCK_POW);\n      };\n\n      for (let gy = 0; gy < rows; gy++) {\n        const py = gy * cellH + cellH / 2;\n        const dy = py - yc;\n        const ady = dy < 0 ? -dy : dy;\n        if (ady > rMax + envHalf) continue; // outside the widest bulge: blank\n        const rowBase = gy * cols;\n\n        for (let gx = 0; gx < cols; gx++) {\n          const px = gx * cellW + cellW / 2;\n          const a = (px - nozzleX) / L; // downstream distance, in shock cells\n          if (a < 0 || a > PLUME_END) continue;\n\n          // free-boundary envelope: the periodic bulge/waist of an\n          // underexpanded jet, dying out downstream\n          const r =\n            0.5 *\n            nozzleD *\n            (1 + 0.55 * Math.sin(tau * a + Math.PI / 2)) *\n            Math.exp(-a / ENV_DECAY);\n\n          if (ady > r) {\n            // faint trace of the jet boundary itself — the waisted envelope\n            if (ady - r < envHalf) {\n              const idx = rowBase + gx;\n              charBuf[idx] = 1;\n              bucketLists[0]!.push(idx);\n            }\n            continue;\n          }\n\n          // the two travelling shock families, as phase coordinates\n          const b = dy * invBand;\n          const d1 = phaseDist(a + b);\n          const d2 = phaseDist(a - b);\n          const on1 = d1 < tol;\n          const on2 = d2 < tol;\n          if (!on1 && !on2) continue; // plume interior between shocks: blank\n\n          let s = 0;\n          if (on1) s = profile(d1);\n          if (on2) {\n            const s2 = profile(d2);\n            if (s2 > s) s = s2;\n          }\n          // both families here — this is the diamond node\n          if (on1 && on2) s = Math.min(1, s * NODE_GAIN);\n          s *= Math.exp(-a / INK_DECAY);\n          if (s <= 0.04) continue;\n\n          const ci = Math.max(1, Math.floor(s * (RAMP.length - 1)));\n          const bucket = Math.min(\n            ALPHA_BUCKETS - 1,\n            Math.floor(s * ALPHA_BUCKETS)\n          );\n          const idx = rowBase + gx;\n          charBuf[idx] = ci;\n          bucketLists[bucket]!.push(idx);\n        }\n      }\n\n      ctx.fillStyle = fg;\n      for (let b = 0; b < ALPHA_BUCKETS; b++) {\n        const list = bucketLists[b]!;\n        if (list.length === 0) continue;\n        ctx.globalAlpha = 0.1 + (b / (ALPHA_BUCKETS - 1)) * 0.9;\n        for (let k = 0; k < list.length; k++) {\n          const idx = list[k]!;\n          const gx = idx % cols;\n          const gy = (idx - gx) / cols;\n          ctx.fillText(\n            RAMP[charBuf[idx]!]!,\n            gx * cellW + cellW / 2,\n            gy * cellH + cellH / 2\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    let npr = nprDesign; // eased nozzle pressure ratio — the throttle\n    let nprTarget = nprDesign;\n    let vector = 0; // eased thrust-vector offset in px\n    let vectorTarget = 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      const k = Math.min(1, dt / THROTTLE_TAU);\n      npr += (nprTarget - npr) * k;\n      vector += (vectorTarget - vector) * k;\n\n      // resting breath: a small jitter in cell spacing, never a scroll\n      const machEff =\n        machFromNpr(npr) *\n        (1 + PULSE_A * Math.sin(t * 0.85) + PULSE_B * Math.sin(t * 2.3));\n\n      draw(t, machEff, vector);\n      if (!document.hidden) raf = requestAnimationFrame(loop);\n    };\n\n    const onPointerMove = (e: PointerEvent) => {\n      const rect = root.getBoundingClientRect();\n      if (rect.width < 2 || rect.height < 2) return;\n      const fx = Math.min(1, Math.max(0, (e.clientX - rect.left) / rect.width));\n      nprTarget = nprMin + fx * (nprMax - nprMin);\n      const fy = (e.clientY - rect.top - rect.height / 2) / (rect.height * 0.25);\n      vectorTarget = Math.min(1, Math.max(-1, fy)) * vectorMax;\n    };\n    const onPointerLeave = () => {\n      nprTarget = nprDesign;\n      vectorTarget = 0;\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, machDesign, 0);\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        draw(0, machDesign, 0);\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, mach]);\n\n  return (\n    <div\n      ref={rootRef}\n      className={`relative isolate h-full 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=\"absolute inset-0 z-10 flex 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-shock-diamonds.tsx"
    }
  ],
  "meta": {
    "collection": "loud",
    "tags": [
      "hero",
      "ascii",
      "canvas",
      "cursor",
      "shock",
      "supersonic",
      "mach"
    ],
    "instruction": "Build <ShockTrain cellSize? mach? children? className?> as a full-bleed Canvas 2D hero that reproduces the shock-cell structure of an underexpanded supersonic jet, not a decorative diagonal hatch. THE WHOLE DESIGN IS SIZED SO THE DIAMONDS READ AS DIAMONDS AT DESKTOP SCALE — a tall band of chevrons, never a thin horizontal streak. GEOMETRY: a nozzle exit of diameter D at x0 = 0.10*W on the jet axis yc = 0.26*H. The axis sits HIGH, not at mid-height, because the optional children are anchored bottom-left; the plume band and the copy must occupy disjoint regions of the frame with no scrim, blur or fade between them. D is the one free geometric parameter and it sets how TALL the train is (the shock band is roughly 1.5*D high), so it is pushed as large as the frame allows: solved at resize as min(0.28*H, 0.98*(W-x0)/(3.2*k)) with k = 1.30*sqrt(Mdesign^2 - 1), and at least 8 grid rows. The cell COUNT is what gives way — only about 3.5 shock cells lay across a 16:10 frame (three crossings plus the lip), rising to about 5 on short wide frames where the 0.28*H cap binds first, which is the point: three or four legible diamonds beat six horizontal dashes. D is fixed hardware and is pinned to the design Mach, never the live one, which is precisely why a throttle change moves the cell length instead of the nozzle. The axis is snapped to a ROW CENTRE — the on-axis node sits exactly at y = yc, and if that lands between two rows the brightest cell in the picture is never sampled and the train degrades into a hatch. DESIGN MACH IS LOW BY DESIGN: default 1.5 (clamped 1.2..4.5). At M 1.5 the Mach angle mu = asin(1/M) is 41.8 degrees, so tan(mu) = 0.89 and the two shock families cross at close to a right angle — the arms read as chevrons. A high design Mach is the failure mode this component exists to avoid: at M 2.4 tan(mu) is 0.46, the arms lie down almost flat, they merge into horizontal streaks, and the short D that a high cell count then forces leaves the band only a handful of rows tall. PLUME BOUNDARY: r(x) = 0.5*D*(1 + 0.55*sin(2*pi*(x-x0)/L + pi/2)) * exp(-(x-x0)/(4.5*L)) — the periodic bulge/waist of a real free jet, decaying downstream; it is traced at the faintest ramp level only where a cell is within about half a row of it, so the envelope reads as a whisper, not an outline. SHOCK-CELL LENGTH is the Prandtl-Pack relation L = 1.30 * D * sqrt(M^2 - 1). SHOCK LINES: the oblique shock leaves the nozzle lip at the Mach angle and reflects specularly off the free boundary, so the whole train collapses to two travelling phase coordinates, p1 = (x-x0)/L + (y-yc)/(L*tan(mu)) and p2 = (x-x0)/L - (y-yc)/(L*tan(mu)); a cell is on a shock when frac(p1) or frac(p2) is within 0.035 of an integer. Ink strength is the max over the two families of pow(1 - dist/0.035, 1.6), multiplied by a downstream decay exp(-(x-x0)/(6.0*L)) — a slow decay, so the third and fourth crossings survive at readable opacity instead of ghosting out — and HARD-MASKED to zero outside |y - yc| > r(x): the shocks exist only inside the plume. WHERE BOTH FAMILIES ARE WITHIN TOLERANCE the strength is multiplied by 1.9 and clamped to 1: that crossing is the diamond node, and it is the entire reason the frame reads as a string of bright X's on the centreline rather than a hatch. RASTERIZATION: 0.035 cell-lengths of phase is thinner than one glyph row, so the hit test is conservatively dilated to at least 0.62 of a cell measured in phase units (hypot(1/L, 1/(L*tan(mu))) is the phase gradient magnitude); everything inside the dilation is full-strength line and the physical 0.035 profile fades over the last stretch — without this the lines break into dashes. SPARSITY: only two thin line families inside a tapering wedge are ever inked; the entire frame outside the plume, and the plume's own interior between shocks, stay blank — measured ink coverage around 3% of the frame. MEASURED at 1440x900 with cellSize 12 and the defaults: D = 252px, L = 366px, 3.56 cells across, the plume occupies 33 of 75 glyph rows and the arms above half opacity occupy 17 rows, all of it in the top 48% of the frame. AMBIENT MOTION: the train breathes with a calm resting pulse, M_eff = M * (1 + 0.018*sin(t*0.85) + 0.008*sin(t*2.3)) — a small jitter in cell spacing, never a scroll or a march. POINTER — the pointer is the THROTTLE: its normalized x maps to a nozzle pressure ratio spanning 0.66x to 2.13x the design NPR — for the default Mach 1.5 the design NPR is (1 + 0.2*M^2)^3.5 = 3.67, so the throttle sweeps NPR from 2.42 to 7.82 and M from 1.20 to 2.00, with the design plume sitting at the centre of pointer travel and L stretching from 0.59x to 1.55x its design value. The window must be anchored to the design NPR rather than to a fixed absolute span: a literal low end solves subsonic and there is no shock structure to draw at all. NPR is eased with a 0.6s time constant and M is re-solved from it isentropically as M = sqrt(5*(npr^0.2857 - 1)) at gamma 1.4, clamped to at least 1.05. Higher NPR means a higher M, a longer L and a shallower Mach angle, so the diamonds visibly stretch apart and the plume lengthens as the pointer moves right and compress as it moves left. Pointer y within +/-0.25*H tilts the jet axis by up to 0.04*H (thrust vectoring), quantized to whole rows so the node stays on a row centre wherever it is aimed, and eased with the same time constant; the travel is deliberately small so a downward vector never pushes the band into the copy. On pointerleave both ease back to the design plume (M 1.5, yc 0.26*H). RENDER: direct-DOM rAF, zero React state on the hot path; two-pass into a Uint8Array ramp-index buffer with 6 luminance buckets and exactly one ctx.globalAlpha write per bucket, from the shared ' .:-=+*#%@' ramp. Ink is read via getComputedStyle(canvas).color and re-derived on a documentElement class MutationObserver so theme flips are live; the mono cell is measured with an offscreen canvas measureText after document.fonts.ready. prefers-reduced-motion renders exactly one static frame at t=0 with M at the design value and no vectoring, and skips both the rAF loop and the pointer listeners. The loop pauses on document.hidden and resumes on visibilitychange. Optional children render bottom-left anchored, in the empty lower half of the frame that the raised jet axis leaves for them — the copy is never crossed by the plume, and no scrim, blur or gradient is used to separate them. Props: cellSize (grid cell px, default 12), mach (design Mach number at rest, default 1.5, clamped to 1.2..4.5), children, className."
  },
  "type": "registry:ui"
}