{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ascii-shapes",
  "title": "ASCII Shapes",
  "description": "7 animated ASCII art components (Spiral, Rose, Wave, Vortex, Pulse, Matrix, Grid) with 5 character sets, 4 sizes, and SSR-safe static mode",
  "dependencies": [],
  "registryDependencies": [
    "@boldkit/utils"
  ],
  "files": [
    {
      "path": "registry/default/ui/ascii-shapes.tsx",
      "content": "import * as React from 'react'\nimport { cn } from '@/lib/utils'\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type AsciiSize = 'sm' | 'md' | 'lg' | 'hero'\nexport type AsciiCharset = 'blocks' | 'braille' | 'classic' | 'line' | 'dots'\nexport type AsciiSpeed = 'slow' | 'normal' | 'fast'\n\nexport interface AsciiShapeProps extends React.HTMLAttributes<HTMLPreElement> {\n  size?: AsciiSize\n  charset?: AsciiCharset\n  color?: string\n  speed?: AsciiSpeed\n  animated?: boolean\n  multicolor?: boolean\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst SIZE_MAP: Record<AsciiSize, { cols: number; rows: number }> = {\n  sm:   { cols: 24,  rows: 12 },\n  md:   { cols: 48,  rows: 24 },\n  lg:   { cols: 72,  rows: 36 },\n  hero: { cols: 120, rows: 60 },\n}\n\nconst CHARSETS: Record<AsciiCharset, string[]> = {\n  blocks:  [' ', '░', '▒', '▓', '█'],\n  braille: [' ', '⠁', '⠃', '⠇', '⠿', '⠷'],\n  classic: [' ', '.', ':', 'o', '*', '#', '@'],\n  line:    [' ', '-', '/', '|', '\\\\', '+', 'X'],\n  dots:    [' ', '.', '·', '•', '●'],\n}\n\nconst SPEED_MAP: Record<AsciiSpeed, number> = {\n  slow:   0.4,\n  normal: 1.0,\n  fast:   2.2,\n}\n\nconst MULTICOLOR_PALETTE = [\n  'hsl(var(--primary))',\n  'hsl(var(--secondary))',\n  'hsl(var(--accent))',\n  'hsl(var(--warning))',\n  'hsl(var(--info))',\n  'hsl(var(--success))',\n]\n\n// ============================================================================\n// Grid engine\n// ============================================================================\n\nfunction makeGrid(cols: number, rows: number): string[][] {\n  return Array.from({ length: rows }, () => Array(cols).fill(' '))\n}\n\nfunction gridToLines(grid: string[][]): string[] {\n  return grid.map((row) => row.join(''))\n}\n\n// ============================================================================\n// Draw functions\n// ============================================================================\n\nfunction drawSpiral(grid: string[][], cols: number, rows: number, t: number, chars: string[]): void {\n  const cx = cols / 2, cy = rows / 2\n  const aspect = 2.0\n  const spacing = Math.min(cx * aspect, cy) * 0.35\n\n  for (let r = 0; r < rows; r++) {\n    for (let c = 0; c < cols; c++) {\n      const dx = (c - cx) * aspect\n      const dy = r - cy\n      const dist = Math.sqrt(dx * dx + dy * dy)\n      if (dist < 0.5) { grid[r][c] = chars[chars.length - 1]; continue }\n      const angle = ((Math.atan2(dy, dx) + t * 0.002) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI)\n      const armPhase = angle / (2 * Math.PI)\n      const winding = Math.round(dist / spacing - armPhase)\n      const nearestR = spacing * (armPhase + winding)\n      const distToArm = Math.abs(dist - nearestR)\n      const threshold = spacing * 0.38\n      if (nearestR >= 0 && distToArm < threshold) {\n        const intensity = 1 - distToArm / threshold\n        grid[r][c] = chars[Math.floor(intensity * (chars.length - 1))]\n      } else {\n        grid[r][c] = ' '\n      }\n    }\n  }\n}\n\nfunction drawRose(grid: string[][], cols: number, rows: number, t: number, chars: string[]): void {\n  const cx = cols / 2, cy = rows / 2\n  const aspect = 2.0\n  const k = 5\n  const radius = Math.min(cx * aspect, cy) * 0.85\n\n  for (let r = 0; r < rows; r++) {\n    for (let c = 0; c < cols; c++) {\n      const dx = (c - cx) * aspect\n      const dy = r - cy\n      const dist = Math.sqrt(dx * dx + dy * dy)\n      const angle = Math.atan2(dy, dx) + t * 0.001\n      const roseR = radius * Math.abs(Math.cos(k * angle))\n      const distToRose = Math.abs(dist - roseR)\n      const threshold = radius * 0.12\n      if (distToRose < threshold) {\n        const intensity = 1 - distToRose / threshold\n        grid[r][c] = chars[Math.floor(intensity * (chars.length - 1))]\n      } else {\n        grid[r][c] = ' '\n      }\n    }\n  }\n}\n\nfunction drawWave(grid: string[][], cols: number, rows: number, t: number, chars: string[]): void {\n  for (let r = 0; r < rows; r++) {\n    for (let c = 0; c < cols; c++) {\n      const x = c / cols\n      const y = r / rows\n      const wave =\n        0.5 * Math.sin(2 * Math.PI * (x * 2 - t * 0.001)) +\n        0.3 * Math.sin(2 * Math.PI * (x * 3 - t * 0.0015)) +\n        0.2 * Math.sin(2 * Math.PI * (x * 5 - t * 0.002))\n      const waveY = 0.5 + wave * 0.3\n      const distToWave = Math.abs(y - waveY)\n      const threshold = 0.08\n      if (distToWave < threshold) {\n        const intensity = 1 - distToWave / threshold\n        grid[r][c] = chars[Math.floor(intensity * (chars.length - 1))]\n      } else {\n        grid[r][c] = ' '\n      }\n    }\n  }\n}\n\nfunction drawVortex(grid: string[][], cols: number, rows: number, t: number, chars: string[]): void {\n  const cx = cols / 2, cy = rows / 2\n  const aspect = 2.0\n  const maxR = Math.sqrt((cx * aspect) ** 2 + cy ** 2)\n\n  for (let r = 0; r < rows; r++) {\n    for (let c = 0; c < cols; c++) {\n      const dx = (c - cx) * aspect\n      const dy = r - cy\n      const dist = Math.sqrt(dx * dx + dy * dy)\n      const angle = Math.atan2(dy, dx)\n      const vortexAngle = angle + dist * 0.3 - t * 0.002\n      const intensity = ((Math.sin(vortexAngle * 3) + 1) / 2) * Math.exp(-dist / (maxR * 0.8))\n      if (intensity > 0.08) {\n        grid[r][c] = chars[Math.min(Math.floor(intensity * (chars.length - 1)), chars.length - 1)]\n      } else {\n        grid[r][c] = ' '\n      }\n    }\n  }\n}\n\nfunction drawPulse(grid: string[][], cols: number, rows: number, t: number, chars: string[]): void {\n  const cx = cols / 2, cy = rows / 2\n  const aspect = 2.0\n  const speed = t * 0.02\n  const ringSpacing = Math.min(cx * aspect, cy) * 0.35\n  const maxR = Math.sqrt((cx * aspect) ** 2 + cy ** 2)\n\n  for (let r = 0; r < rows; r++) {\n    for (let c = 0; c < cols; c++) {\n      const dx = (c - cx) * aspect\n      const dy = r - cy\n      const dist = Math.sqrt(dx * dx + dy * dy)\n      const phase = ((dist - speed) % ringSpacing + ringSpacing) % ringSpacing\n      const ringIntensity = Math.pow(Math.sin(Math.PI * phase / ringSpacing), 2)\n      const fade = Math.max(0, 1 - dist / maxR)\n      const intensity = ringIntensity * fade\n      if (intensity > 0.05) {\n        grid[r][c] = chars[Math.floor(intensity * (chars.length - 1))]\n      } else {\n        grid[r][c] = ' '\n      }\n    }\n  }\n}\n\ninterface ColState { offset: number; speed: number }\n\nfunction makeMatrixState(cols: number): ColState[] {\n  return Array.from({ length: cols }, (_, i) => ({\n    offset: (i * 37 % 100),\n    speed: 0.5 + (i * 13 % 10) * 0.15,\n  }))\n}\n\nfunction drawMatrix(grid: string[][], cols: number, rows: number, t: number, chars: string[], state: ColState[]): void {\n  for (let c = 0; c < cols; c++) {\n    const colT = (t * 0.001 * state[c].speed + state[c].offset) % (rows * 1.8)\n    for (let r = 0; r < rows; r++) {\n      const distFromHead = colT - r\n      if (distFromHead >= 0 && distFromHead < 1) {\n        grid[r][c] = chars[chars.length - 1]\n      } else if (distFromHead >= 1 && distFromHead < rows * 0.45) {\n        const fade = 1 - distFromHead / (rows * 0.45)\n        grid[r][c] = chars[Math.floor(fade * (chars.length - 1))]\n      } else {\n        grid[r][c] = ' '\n      }\n    }\n  }\n}\n\nfunction drawGrid(grid: string[][], cols: number, rows: number, t: number, chars: string[]): void {\n  const cellW = 6, cellH = 3\n  for (let r = 0; r < rows; r++) {\n    for (let c = 0; c < cols; c++) {\n      const isH = r % cellH === 0\n      const isV = c % cellW === 0\n      if (isH && isV) {\n        const wave = (Math.sin(c / cols * Math.PI * 4 + r / rows * Math.PI * 2 - t * 0.002) + 1) / 2\n        grid[r][c] = chars[Math.floor(wave * (chars.length - 1))]\n      } else if (isH || isV) {\n        const wave = Math.sin(c / cols * Math.PI * 8 + r / rows * Math.PI * 4 - t * 0.001) * 0.3 + 0.3\n        grid[r][c] = chars[Math.floor(wave * (chars.length - 1))]\n      } else {\n        grid[r][c] = ' '\n      }\n    }\n  }\n}\n\nfunction drawTorus(grid: string[][], cols: number, rows: number, t: number, chars: string[]): void {\n  const A = t * 0.0012, B = t * 0.0007\n  const cosA = Math.cos(A), sinA = Math.sin(A)\n  const cosB = Math.cos(B), sinB = Math.sin(B)\n  const R1 = 1.0, R2 = 2.2, K2 = 5.0\n  const cx = cols / 2, cy = rows / 2\n  const aspect = 0.5\n  const screenScale = Math.min(cols * aspect, rows) * 0.85\n  const K1 = screenScale * K2 / (K2 + R1 + R2)\n  const zbuf: number[][] = Array.from({ length: rows }, () => Array(cols).fill(-Infinity))\n  for (let theta = 0; theta < 2 * Math.PI; theta += 0.05) {\n    const cosT = Math.cos(theta), sinT = Math.sin(theta)\n    for (let phi = 0; phi < 2 * Math.PI; phi += 0.02) {\n      const cosP = Math.cos(phi), sinP = Math.sin(phi)\n      const ox = (R2 + R1 * cosT) * cosP\n      const oy = (R2 + R1 * cosT) * sinP\n      const oz = R1 * sinT\n      const oy1 =  oy * cosA - oz * sinA\n      const oz1 =  oy * sinA + oz * cosA\n      const ox2 =  ox * cosB - oy1 * sinB\n      const oy2 =  ox * sinB + oy1 * cosB\n      const oz2 =  oz1\n      const zDist = K2 - oz2\n      if (zDist <= 0) continue\n      const ooz = 1.0 / zDist\n      const xp = Math.round(cx + K1 * ox2 * ooz)\n      const yp = Math.round(cy - K1 * oy2 * ooz * aspect)\n      if (xp < 0 || xp >= cols || yp < 0 || yp >= rows) continue\n      const nx = cosT * cosP, ny = cosT * sinP, nz = sinT\n      const ny1 =  ny * cosA - nz * sinA\n      const nz1 =  ny * sinA + nz * cosA\n      const nx2 =  nx * cosB - ny1 * sinB\n      const ny2 =  nx * sinB + ny1 * cosB\n      const nz2 =  nz1\n      const L = nx2 * 0.57 + ny2 * 0.57 + nz2 * (-0.57)\n      if (L > 0 && ooz > zbuf[yp][xp]) {\n        zbuf[yp][xp] = ooz\n        grid[yp][xp] = chars[Math.min(Math.floor(L * (chars.length - 1)), chars.length - 1)]\n      }\n    }\n  }\n}\n\nfunction drawSphere(grid: string[][], cols: number, rows: number, t: number, chars: string[]): void {\n  const cx = cols / 2, cy = rows / 2\n  const aspect = 2.0\n  const R = Math.min(cols / (aspect * 2), rows / 2) * 0.88\n  const A = t * 0.0007, B = t * 0.0004\n  const cosA = Math.cos(A), sinA = Math.sin(A)\n  const cosB = Math.cos(B), sinB = Math.sin(B)\n  // Light from upper-right-front\n  const lx = 0.577, ly = -0.577, lz = -0.577\n\n  for (let r = 0; r < rows; r++) {\n    for (let c = 0; c < cols; c++) {\n      const sx = (c - cx) / (R * aspect)\n      const sy = (r - cy) / R\n      const d2 = sx * sx + sy * sy\n      if (d2 >= 1.0) continue\n      const sz = Math.sqrt(1.0 - d2)  // front-facing z (positive toward viewer)\n      // Rotate surface point for texture coordinates\n      const ny1 = sy * cosA - sz * sinA\n      const nz1 = sy * sinA + sz * cosA\n      const nx2 = sx * cosB + nz1 * sinB\n      const nz2 = -sx * sinB + nz1 * cosB\n      const ny2 = ny1\n      // Lat/lon grid lines on rotating surface\n      const lat = Math.asin(Math.max(-1, Math.min(1, ny2)))\n      const lon = Math.atan2(nx2, nz2)\n      const onGrid = Math.abs(Math.cos(lat * 5)) > 0.93 || Math.abs(Math.cos(lon * 8)) > 0.91\n      // Lighting uses unrotated normal (sx, sy, sz)\n      const L = Math.max(0, sx * lx + sy * ly + sz * lz)\n      if (onGrid) {\n        const intensity = 0.3 + L * 0.7\n        grid[r][c] = chars[Math.min(Math.floor(intensity * (chars.length - 1)), chars.length - 1)]\n      } else if (L > 0.02) {\n        grid[r][c] = chars[Math.min(Math.floor(L * (chars.length - 1)), chars.length - 1)]\n      }\n      // dark side stays ' '\n    }\n  }\n}\n\nfunction drawCube(grid: string[][], cols: number, rows: number, t: number, chars: string[]): void {\n  const A = t * 0.0009, B = t * 0.0006\n  const cosA = Math.cos(A), sinA = Math.sin(A)\n  const cosB = Math.cos(B), sinB = Math.sin(B)\n  const cx = cols / 2, cy = rows / 2\n  const K2 = 5.0, S = 1.4, aspect = 0.5\n  const scale = Math.min(cols * aspect, rows) * 0.72\n  const K1 = scale * K2 / (K2 + S)\n  const zbuf: number[][] = Array.from({ length: rows }, () => Array(cols).fill(-Infinity))\n  const lx = 0.577, ly = -0.577, lz = -0.577\n\n  function rot(px: number, py: number, pz: number): [number, number, number] {\n    const y1 = py * cosA - pz * sinA\n    const z1 = py * sinA + pz * cosA\n    return [px * cosB + z1 * sinB, y1, -px * sinB + z1 * cosB]\n  }\n\n  const step = 0.04\n  const faces: Array<[[number, number, number], (u: number, v: number) => [number, number, number]]> = [\n    [[0,  0,  1], (u, v) => [ u*S,  v*S,  S]],\n    [[0,  0, -1], (u, v) => [-u*S,  v*S, -S]],\n    [[1,  0,  0], (u, v) => [ S,    v*S, -u*S]],\n    [[-1, 0,  0], (u, v) => [-S,    v*S,  u*S]],\n    [[0, -1,  0], (u, v) => [ u*S, -S,    v*S]],\n    [[0,  1,  0], (u, v) => [ u*S,  S,   -v*S]],\n  ]\n\n  for (const [norm, ptFn] of faces) {\n    const [rnx, rny, rnz] = rot(norm[0], norm[1], norm[2])\n    const L = Math.max(0, rnx * lx + rny * ly + rnz * lz)\n    if (L < 0.02) continue\n    const ch = chars[Math.min(Math.floor(L * (chars.length - 1)), chars.length - 1)]\n    for (let u = -1; u <= 1 + step * 0.5; u += step) {\n      for (let v = -1; v <= 1 + step * 0.5; v += step) {\n        const [rx, ry, rz] = rot(...ptFn(u, v))\n        const zDist = K2 - rz\n        if (zDist <= 0) continue\n        const ooz = 1 / zDist\n        const xp = Math.round(cx + K1 * rx * ooz)\n        const yp = Math.round(cy - K1 * ry * ooz * aspect)\n        if (xp >= 0 && xp < cols && yp >= 0 && yp < rows && ooz > zbuf[yp][xp]) {\n          zbuf[yp][xp] = ooz\n          grid[yp][xp] = ch\n        }\n      }\n    }\n  }\n}\n\nfunction drawDonut(grid: string[][], cols: number, rows: number, t: number, chars: string[]): void {\n  // Faithful a1k0n donut.c algorithm — ring lies in XZ plane, tube depth = sinTheta,\n  // so the hole is ALWAYS visible from any viewing angle.\n  // A tilts around X-axis, B spins around Z-axis. Luminance from a1k0n's exact formula.\n  const A = t * 0.00083, B = t * 0.00125\n  const cosA = Math.cos(A), sinA = Math.sin(A)\n  const cosB = Math.cos(B), sinB = Math.sin(B)\n  const R1 = 1.0, R2 = 2.0, K2 = 5.0\n  const cx = cols / 2, cy = rows / 2\n  const aspect = 0.5\n  const K1 = Math.min(cols * aspect, rows) * K2 / (K2 + R1 + R2) * 0.9\n  const zbuf: number[][] = Array.from({ length: rows }, () => Array(cols).fill(-Infinity))\n\n  // theta: angle around tube cross-section\n  // phi:   angle around the ring center\n  for (let theta = 0; theta < 2 * Math.PI; theta += 0.07) {\n    const cosT = Math.cos(theta), sinT = Math.sin(theta)\n    const h = R2 + R1 * cosT  // radial distance from axis to surface point\n\n    for (let phi = 0; phi < 2 * Math.PI; phi += 0.02) {\n      const cosP = Math.cos(phi), sinP = Math.sin(phi)\n\n      // y-component after rotating by A around X-axis (before B spin)\n      const ycomp = sinP * h * cosA - sinT * sinA\n\n      // z depth after A rotation (determines perspective + occlusion)\n      const zDist = sinP * h * sinA + sinT * cosA + K2\n      if (zDist <= 0) continue\n      const ooz = 1 / zDist\n\n      // Project to screen (B rotates around Z in screen space)\n      const xp = Math.round(cx + K1 * ooz * (cosP * h * cosB - ycomp * sinB))\n      const yp = Math.round(cy - K1 * ooz * (cosP * h * sinB + ycomp * cosB) * aspect)\n      if (xp < 0 || xp >= cols || yp < 0 || yp >= rows) continue\n\n      // a1k0n's luminance: dot(rotated_normal, light_direction)\n      // Produces smooth gradient across the tube with strong highlight on top\n      const L = (sinT * sinA - sinP * cosT * cosA) * cosB\n              - sinP * cosT * sinA\n              - sinT * cosA\n              - cosP * cosT * sinB\n\n      if (L > 0 && ooz > zbuf[yp][xp]) {\n        zbuf[yp][xp] = ooz\n        grid[yp][xp] = chars[Math.min(Math.floor(L * (chars.length - 1)), chars.length - 1)]\n      }\n    }\n  }\n}\n\nfunction drawHelix(grid: string[][], cols: number, rows: number, t: number, chars: string[]): void {\n  const cx = cols / 2, cy = rows / 2\n  const aspect = 0.5\n  const K2 = 6.0\n  const scale = Math.min(cols * aspect, rows) * 0.5\n  const K1 = scale\n  const B = t * 0.0006\n  const cosB = Math.cos(B), sinB = Math.sin(B)\n  const zbuf: number[][] = Array.from({ length: rows }, () => Array(cols).fill(-Infinity))\n  const lx = 0.707, lz = -0.707\n  const helixR = 1.2, helixH = 3.8, turns = 3\n\n  function plot(px: number, py: number, pz: number, intensity: number) {\n    const rx = px * cosB + pz * sinB\n    const rz = -px * sinB + pz * cosB\n    const zDist = K2 - rz\n    if (zDist <= 0) return\n    const ooz = 1 / zDist\n    const xp = Math.round(cx + K1 * rx * ooz)\n    const yp = Math.round(cy - K1 * py * ooz * aspect)\n    if (xp >= 0 && xp < cols && yp >= 0 && yp < rows && ooz > zbuf[yp][xp]) {\n      zbuf[yp][xp] = ooz\n      grid[yp][xp] = chars[Math.min(Math.floor(intensity * (chars.length - 1)), chars.length - 1)]\n    }\n  }\n\n  const pStep = 0.025\n  for (let phi = 0; phi < turns * 2 * Math.PI; phi += pStep) {\n    const y = (phi / (turns * 2 * Math.PI) - 0.5) * helixH * 2\n    // Strand 1\n    const x1 = helixR * Math.cos(phi), z1 = helixR * Math.sin(phi)\n    plot(x1, y, z1, Math.max(0.15, Math.cos(phi) * lx + Math.sin(phi) * lz * 0.5 + 0.55))\n    // Strand 2 (π offset)\n    const x2 = helixR * Math.cos(phi + Math.PI), z2 = helixR * Math.sin(phi + Math.PI)\n    plot(x2, y, z2, Math.max(0.15, Math.cos(phi + Math.PI) * lx + Math.sin(phi + Math.PI) * lz * 0.5 + 0.55))\n    // Rungs every half-turn\n    if (phi % (Math.PI * 0.5) < pStep * 1.5) {\n      for (let s = 0; s <= 14; s++) {\n        const f = s / 14\n        plot(x1 + (x2 - x1) * f, y, z1 + (z2 - z1) * f, 0.45)\n      }\n    }\n  }\n}\n\nfunction drawTrefoilKnot(grid: string[][], cols: number, rows: number, t: number, chars: string[]): void {\n  const A = t * 0.0008, B = t * 0.0005\n  const cosA = Math.cos(A), sinA = Math.sin(A)\n  const cosB = Math.cos(B), sinB = Math.sin(B)\n  const cx = cols / 2, cy = rows / 2\n  const aspect = 0.5\n  const K2 = 5.0\n  const screenScale = Math.min(cols * aspect, rows) * 0.85\n  const K1 = screenScale * K2 / (K2 + 3.5)\n  const zbuf: number[][] = Array.from({ length: rows }, () => Array(cols).fill(-Infinity))\n  const tubeR = 0.28\n  const steps = 400\n  const tubeSteps = 20\n  const lx = 0.577, ly = 0.577, lz = -0.577\n  const EPS = 0.02\n\n  function knotPt(th: number): [number, number, number] {\n    return [\n      (2 + Math.cos(3 * th)) * Math.cos(2 * th),\n      (2 + Math.cos(3 * th)) * Math.sin(2 * th),\n      Math.sin(3 * th) * 2,\n    ]\n  }\n\n  function rotYX(px: number, py: number, pz: number): [number, number, number] {\n    const rx1 = px * cosA + pz * sinA\n    const ry1 = py\n    const rz1 = -px * sinA + pz * cosA\n    return [rx1, ry1 * cosB - rz1 * sinB, ry1 * sinB + rz1 * cosB]\n  }\n\n  for (let i = 0; i < steps; i++) {\n    const th = (i / steps) * 2 * Math.PI\n    const [px, py, pz] = knotPt(th)\n    const [px2, py2, pz2] = knotPt(th + EPS)\n\n    let tx = px2 - px, ty = py2 - py, tz = pz2 - pz\n    const tlen = Math.sqrt(tx * tx + ty * ty + tz * tz)\n    tx /= tlen; ty /= tlen; tz /= tlen\n\n    let upx = 0, upy = 0, upz = 1\n    let dot = tx * upx + ty * upy + tz * upz\n    let nx = upx - dot * tx, ny = upy - dot * ty, nz = upz - dot * tz\n    let nlen = Math.sqrt(nx * nx + ny * ny + nz * nz)\n    if (nlen < 0.001) {\n      upx = 0; upy = 1; upz = 0\n      dot = tx * upx + ty * upy + tz * upz\n      nx = upx - dot * tx; ny = upy - dot * ty; nz = upz - dot * tz\n      nlen = Math.sqrt(nx * nx + ny * ny + nz * nz)\n    }\n    nx /= nlen; ny /= nlen; nz /= nlen\n    const bx = ty * nz - tz * ny, by = tz * nx - tx * nz, bz = tx * ny - ty * nx\n\n    for (let j = 0; j < tubeSteps; j++) {\n      const u = (j / tubeSteps) * 2 * Math.PI\n      const cu = Math.cos(u), su = Math.sin(u)\n      const qx = px + tubeR * (cu * nx + su * bx)\n      const qy = py + tubeR * (cu * ny + su * by)\n      const qz = pz + tubeR * (cu * nz + su * bz)\n\n      const [rx, ry, rz] = rotYX(qx, qy, qz)\n      const zDist = K2 - rz\n      if (zDist <= 0) continue\n      const ooz = 1 / zDist\n      const xp = Math.round(cx + K1 * rx * ooz)\n      const yp = Math.round(cy - K1 * ry * ooz * aspect)\n      if (xp < 0 || xp >= cols || yp < 0 || yp >= rows) continue\n\n      const snx = cu * nx + su * bx, sny = cu * ny + su * by, snz = cu * nz + su * bz\n      const [rnx, rny, rnz] = rotYX(snx, sny, snz)\n      const L = Math.max(0, rnx * lx + rny * ly + rnz * lz)\n\n      if (ooz > zbuf[yp][xp]) {\n        zbuf[yp][xp] = ooz\n        const intensity = 0.15 + L * 0.85\n        grid[yp][xp] = chars[Math.min(Math.floor(intensity * (chars.length - 1)), chars.length - 1)]\n      }\n    }\n  }\n}\n\nfunction drawGeodesicDome(grid: string[][], cols: number, rows: number, t: number, chars: string[]): void {\n  const A = t * 0.0004, B = t * 0.00025\n  const cosA = Math.cos(A), sinA = Math.sin(A)\n  const cosB = Math.cos(B), sinB = Math.sin(B)\n  const cx = cols / 2, cy = rows / 2\n  const aspect = 0.5\n  const K2 = 5.0\n  const screenScale = Math.min(cols * aspect, rows) * 0.85\n  const K1 = screenScale * K2 / (K2 + 1.05)\n  const zbuf: number[][] = Array.from({ length: rows }, () => Array(cols).fill(-Infinity))\n  const lx = 0.577, ly = 0.577, lz = -0.577\n\n  function norm3(v: [number, number, number]): [number, number, number] {\n    const len = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])\n    return [v[0] / len, v[1] / len, v[2] / len]\n  }\n\n  function rotYX(px: number, py: number, pz: number): [number, number, number] {\n    const rx1 = px * cosA + pz * sinA\n    const ry1 = py\n    const rz1 = -px * sinA + pz * cosA\n    return [rx1, ry1 * cosB - rz1 * sinB, ry1 * sinB + rz1 * cosB]\n  }\n\n  const phi = (1 + Math.sqrt(5)) / 2\n  const rawV: [number, number, number][] = [\n    [0, 1, phi], [0, -1, phi], [0, 1, -phi], [0, -1, -phi],\n    [1, phi, 0], [-1, phi, 0], [1, -phi, 0], [-1, -phi, 0],\n    [phi, 0, 1], [-phi, 0, 1], [phi, 0, -1], [-phi, 0, -1],\n  ]\n  const faces: [number, number, number][] = [\n    [0,1,8],[0,8,4],[0,4,5],[0,5,9],[0,9,1],\n    [1,6,8],[8,10,4],[4,2,5],[5,11,9],[9,7,1],\n    [6,10,8],[10,2,4],[2,11,5],[11,7,9],[7,6,1],\n    [3,6,7],[3,10,6],[3,2,10],[3,11,2],[3,7,11],\n  ]\n\n  const freq = 3\n\n  function plotEdge(v1: [number, number, number], v2: [number, number, number]) {\n    if (v1[1] < -0.3 && v2[1] < -0.3) return\n    for (let s = 0; s <= 28; s++) {\n      const f = s / 28\n      const px = v1[0] + f * (v2[0] - v1[0])\n      const py = v1[1] + f * (v2[1] - v1[1])\n      const pz = v1[2] + f * (v2[2] - v1[2])\n      const [rx, ry, rz] = rotYX(px, py, pz)\n      const zDist = K2 - rz\n      if (zDist <= 0) continue\n      const ooz = 1 / zDist\n      const xp = Math.round(cx + K1 * rx * ooz)\n      const yp = Math.round(cy - K1 * ry * ooz * aspect)\n      if (xp < 0 || xp >= cols || yp < 0 || yp >= rows) continue\n      const [rnx, rny, rnz] = rotYX(px, py, pz)\n      const L = Math.max(0.25, rnx * lx + rny * ly + rnz * lz)\n      if (ooz > zbuf[yp][xp]) {\n        zbuf[yp][xp] = ooz\n        grid[yp][xp] = chars[Math.min(Math.floor(L * (chars.length - 1)), chars.length - 1)]\n      }\n    }\n  }\n\n  for (const [ai, bi, ci] of faces) {\n    const va = norm3(rawV[ai]), vb = norm3(rawV[bi]), vc = norm3(rawV[ci])\n    const pts: [number, number, number][][] = []\n    for (let i = 0; i <= freq; i++) {\n      pts[i] = []\n      for (let j = 0; j <= freq - i; j++) {\n        const k = freq - i - j\n        pts[i][j] = norm3([(i * va[0] + j * vb[0] + k * vc[0]) / freq,\n                            (i * va[1] + j * vb[1] + k * vc[1]) / freq,\n                            (i * va[2] + j * vb[2] + k * vc[2]) / freq])\n      }\n    }\n    for (let i = 0; i <= freq; i++) {\n      for (let j = 0; j <= freq - i; j++) {\n        if (j + 1 <= freq - i) plotEdge(pts[i][j], pts[i][j + 1])\n        if (i + 1 <= freq && j <= freq - (i + 1)) plotEdge(pts[i][j], pts[i + 1][j])\n        if (i + 1 <= freq && j >= 1) plotEdge(pts[i][j], pts[i + 1][j - 1])\n      }\n    }\n  }\n}\n\nfunction drawSaturn(grid: string[][], cols: number, rows: number, t: number, chars: string[]): void {\n  const A = t * 0.0007, B = t * 0.0004\n  const cosA = Math.cos(A), sinA = Math.sin(A)\n  const cosB = Math.cos(B), sinB = Math.sin(B)\n  const cx = cols / 2, cy = rows / 2\n  const aspect = 0.5\n  const K2 = 5.0\n  const screenScale = Math.min(cols * aspect, rows) * 0.85\n  const K1 = screenScale * K2 / (K2 + 2.5)\n  const zbuf: number[][] = Array.from({ length: rows }, () => Array(cols).fill(-Infinity))\n  const lx = 0.577, ly = 0.577, lz = -0.577\n  const TILT = 0.47\n  const sinTilt = Math.sin(TILT), cosTilt = Math.cos(TILT)\n\n  function rotYX(px: number, py: number, pz: number): [number, number, number] {\n    const rx1 = px * cosA + pz * sinA\n    const ry1 = py\n    const rz1 = -px * sinA + pz * cosA\n    return [rx1, ry1 * cosB - rz1 * sinB, ry1 * sinB + rz1 * cosB]\n  }\n\n  function plotPoint(px: number, py: number, pz: number, nx: number, ny: number, nz: number) {\n    const [rx, ry, rz] = rotYX(px, py, pz)\n    const zDist = K2 - rz\n    if (zDist <= 0) return\n    const ooz = 1 / zDist\n    const xp = Math.round(cx + K1 * rx * ooz)\n    const yp = Math.round(cy - K1 * ry * ooz * aspect)\n    if (xp < 0 || xp >= cols || yp < 0 || yp >= rows) return\n    const [rnx, rny, rnz] = rotYX(nx, ny, nz)\n    const L = Math.max(0, rnx * lx + rny * ly + rnz * lz)\n    if (L > 0 && ooz > zbuf[yp][xp]) {\n      zbuf[yp][xp] = ooz\n      grid[yp][xp] = chars[Math.min(Math.floor(L * (chars.length - 1)), chars.length - 1)]\n    }\n  }\n\n  // Planet sphere — tighter sampling for denser coverage\n  for (let theta = 0; theta < 2 * Math.PI; theta += 0.04) {\n    const cosT = Math.cos(theta), sinT = Math.sin(theta)\n    for (let phi = 0; phi < 2 * Math.PI; phi += 0.02) {\n      const cosP = Math.cos(phi), sinP = Math.sin(phi)\n      const px = cosT * cosP, py = cosT * sinP, pz = sinT\n      plotPoint(px, py, pz, px, py, pz)\n    }\n  }\n\n  // Rings — solid swept disk sampled at many radii (not just sparse bands)\n  const R_inner = 1.35, R_outer = 2.45\n  const ringSamples = 40\n  for (let ri = 0; ri <= ringSamples; ri++) {\n    const rFrac = ri / ringSamples\n    // Cassini division gap\n    if (rFrac > 0.42 && rFrac < 0.58) continue\n    const r = R_inner + (R_outer - R_inner) * rFrac\n    // Brightness: bright B ring (inner), dark C ring hint, Cassini gap, bright A ring (outer)\n    const ringBrightness = rFrac < 0.42\n      ? 0.35 + rFrac * 1.1   // B ring — bright, brighter toward gap\n      : 0.25 + (1 - rFrac) * 0.9  // A ring — bright at inner edge, fades outward\n\n    for (let theta = 0; theta < 2 * Math.PI; theta += 0.01) {\n      const cosT = Math.cos(theta), sinT = Math.sin(theta)\n      const px = r * cosT\n      const py = -r * sinT * sinTilt\n      const pz =  r * sinT * cosTilt\n      const [rx, ry, rz] = rotYX(px, py, pz)\n      const zDist = K2 - rz\n      if (zDist <= 0) continue\n      const ooz = 1 / zDist\n      const xp = Math.round(cx + K1 * rx * ooz)\n      const yp = Math.round(cy - K1 * ry * ooz * aspect)\n      if (xp < 0 || xp >= cols || yp < 0 || yp >= rows) continue\n      if (ooz > zbuf[yp][xp]) {\n        zbuf[yp][xp] = ooz\n        grid[yp][xp] = chars[Math.min(Math.floor(ringBrightness * (chars.length - 1)), chars.length - 1)]\n      }\n    }\n  }\n}\n\nfunction drawHyperboloid(grid: string[][], cols: number, rows: number, t: number, chars: string[]): void {\n  const A = t * 0.0006, B = t * 0.0003\n  const cosA = Math.cos(A), sinA = Math.sin(A)\n  const cosB = Math.cos(B), sinB = Math.sin(B)\n  const cx = cols / 2, cy = rows / 2\n  const aspect = 0.5\n  const H = 1.7\n  const K2 = 6.0\n  const maxExtent = Math.sqrt(1 + H * H) + 0.1\n  const screenScale = Math.min(cols * aspect, rows) * 0.85\n  const K1 = screenScale * K2 / (K2 + maxExtent)\n  const zbuf: number[][] = Array.from({ length: rows }, () => Array(cols).fill(-Infinity))\n  const lx = 0.577, ly = 0.577, lz = -0.577\n  const nRulings = 32\n  const nPoints = 60\n\n  function rotYX(px: number, py: number, pz: number): [number, number, number] {\n    const rx1 = px * cosA + pz * sinA\n    const ry1 = py\n    const rz1 = -px * sinA + pz * cosA\n    return [rx1, ry1 * cosB - rz1 * sinB, ry1 * sinB + rz1 * cosB]\n  }\n\n  function plotRulingPoint(px: number, py: number, pz: number) {\n    const [rx, ry, rz] = rotYX(px, py, pz)\n    const zDist = K2 - rz\n    if (zDist <= 0) return\n    const ooz = 1 / zDist\n    const xp = Math.round(cx + K1 * rx * ooz)\n    const yp = Math.round(cy - K1 * ry * ooz * aspect)\n    if (xp < 0 || xp >= cols || yp < 0 || yp >= rows) return\n    // Outward normal of hyperboloid x²+z²-y²=1 at (x,y,z) is (x,-y,z)/|(x,-y,z)|\n    const nm = Math.sqrt(px * px + py * py + pz * pz)\n    const [rnx, rny, rnz] = rotYX(px / nm, -py / nm, pz / nm)\n    const L = Math.max(0.12, rnx * lx + rny * ly + rnz * lz)\n    if (ooz > zbuf[yp][xp]) {\n      zbuf[yp][xp] = ooz\n      grid[yp][xp] = chars[Math.min(Math.floor(L * (chars.length - 1)), chars.length - 1)]\n    }\n  }\n\n  // Two families of straight-line rulings on x²+z²-y²=1\n  // Family A: P(s,v) = (cos(s)+v·sin(s), v, sin(s)-v·cos(s))\n  // Family B: P(s,v) = (cos(s)-v·sin(s), v, sin(s)+v·cos(s))\n  for (let k = 0; k < nRulings; k++) {\n    const s = (k / nRulings) * 2 * Math.PI\n    const cosS = Math.cos(s), sinS = Math.sin(s)\n    for (let family = 0; family < 2; family++) {\n      const dir = family === 0 ? 1 : -1\n      for (let j = 0; j <= nPoints; j++) {\n        const v = -H + (2 * H * j) / nPoints\n        plotRulingPoint(cosS + v * dir * sinS, v, sinS - v * dir * cosS)\n      }\n    }\n  }\n\n  // Horizontal rings at multiple levels for structural definition\n  for (let j = 0; j <= 80; j++) {\n    const theta = (j / 80) * 2 * Math.PI\n    for (const vy of [-H, -H * 0.75, -H * 0.5, -H * 0.25, 0, H * 0.25, H * 0.5, H * 0.75, H]) {\n      const r = Math.sqrt(1 + vy * vy)\n      plotRulingPoint(r * Math.cos(theta), vy, r * Math.sin(theta))\n    }\n  }\n}\n\nfunction drawDNA(grid: string[][], cols: number, rows: number, t: number, chars: string[]): void {\n  const A = t * 0.0005\n  const cosA = Math.cos(A), sinA = Math.sin(A)\n  const cx = cols / 2, cy = rows / 2\n  const aspect = 0.5\n  const K2 = 6.0\n  const scale = Math.min(cols * aspect, rows) * 0.52\n  const K1 = scale\n  const zbuf: number[][] = Array.from({ length: rows }, () => Array(cols).fill(-Infinity))\n  const lx = 0.707, lz = -0.707\n  const helixR = 1.0\n  const helixH = 4.2\n  const turns = 4.0\n  const tubeR = 0.22\n  const bpPerTurn = 10\n  const totalSteps = Math.round(turns * bpPerTurn * 6)\n  // B-DNA: strands are ~150° apart (minor groove ~120°, major groove ~240°)\n  const strandOffset = (150 / 180) * Math.PI\n\n  function rotY(px: number, py: number, pz: number): [number, number, number] {\n    return [px * cosA + pz * sinA, py, -px * sinA + pz * cosA]\n  }\n\n  function plotTubeSection(px: number, py: number, pz: number,\n    tx: number, ty: number, tz: number, r: number, baseBrightness: number) {\n    let upx = 0, upy = 0, upz = 1\n    let dot = tx * upx + ty * upy + tz * upz\n    let nx = upx - dot * tx, ny = upy - dot * ty, nz = upz - dot * tz\n    let nlen = Math.sqrt(nx * nx + ny * ny + nz * nz)\n    if (nlen < 0.001) {\n      upx = 0; upy = 1; upz = 0\n      dot = tx * upx + ty * upy + tz * upz\n      nx = upx - dot * tx; ny = upy - dot * ty; nz = upz - dot * tz\n      nlen = Math.sqrt(nx * nx + ny * ny + nz * nz)\n    }\n    nx /= nlen; ny /= nlen; nz /= nlen\n    const bx = ty * nz - tz * ny, by = tz * nx - tx * nz, bz = tx * ny - ty * nx\n\n    for (let j = 0; j < 16; j++) {\n      const u = (j / 16) * 2 * Math.PI\n      const cu = Math.cos(u), su = Math.sin(u)\n      const qx = px + r * (cu * nx + su * bx)\n      const qy = py + r * (cu * ny + su * by)\n      const qz = pz + r * (cu * nz + su * bz)\n      const [rx, ry, rz] = rotY(qx, qy, qz)\n      const zDist = K2 - rz\n      if (zDist <= 0) continue\n      const ooz = 1 / zDist\n      const xp = Math.round(cx + K1 * rx * ooz)\n      const yp = Math.round(cy - K1 * ry * ooz * aspect)\n      if (xp < 0 || xp >= cols || yp < 0 || yp >= rows) continue\n      const snx = cu * nx + su * bx, snz = cu * nz + su * bz\n      const [rnx,, rnz] = rotY(snx, 0, snz)\n      const L = Math.max(0, rnx * lx + rnz * lz)\n      if (ooz > zbuf[yp][xp]) {\n        zbuf[yp][xp] = ooz\n        const intensity = Math.max(0.12, Math.min(1, baseBrightness * (0.3 + L * 0.7)))\n        grid[yp][xp] = chars[Math.min(Math.floor(intensity * (chars.length - 1)), chars.length - 1)]\n      }\n    }\n  }\n\n  const stepsPerBP = 6\n  const totalAngle = turns * 2 * Math.PI\n\n  for (let i = 0; i < totalSteps; i++) {\n    const f = i / totalSteps\n    const y = -helixH + f * helixH * 2\n    const angle = f * totalAngle\n    const da = totalAngle / totalSteps\n\n    // Tangent direction (same for both strands)\n    const txRaw = -helixR * Math.sin(angle) * da\n    const tyRaw = (helixH * 2) / totalSteps\n    const tzRaw = helixR * Math.cos(angle) * da\n    const tlen = Math.sqrt(txRaw * txRaw + tyRaw * tyRaw + tzRaw * tzRaw)\n    const tx = txRaw / tlen, ty = tyRaw / tlen, tz = tzRaw / tlen\n\n    // Strand 1\n    const x1 = helixR * Math.cos(angle), z1 = helixR * Math.sin(angle)\n    plotTubeSection(x1, y, z1, tx, ty, tz, tubeR, 1.0)\n\n    // Strand 2 (B-DNA: 150° offset)\n    const x2 = helixR * Math.cos(angle + strandOffset), z2 = helixR * Math.sin(angle + strandOffset)\n    plotTubeSection(x2, y, z2, tx, ty, tz, tubeR, 0.72)\n\n    // Base-pair rungs every stepsPerBP\n    if (i % stepsPerBP === 0) {\n      for (let s = 0; s <= 10; s++) {\n        const sf = s / 10\n        const rx = x1 + sf * (x2 - x1)\n        const rz = z1 + sf * (z2 - z1)\n        const [vrx, vry, vrz] = rotY(rx, y, rz)\n        const zDist = K2 - vrz\n        if (zDist <= 0) continue\n        const ooz = 1 / zDist\n        const xp = Math.round(cx + K1 * vrx * ooz)\n        const yp = Math.round(cy - K1 * vry * ooz * aspect)\n        if (xp < 0 || xp >= cols || yp < 0 || yp >= rows) continue\n        if (ooz > zbuf[yp][xp]) {\n          zbuf[yp][xp] = ooz\n          grid[yp][xp] = chars[Math.min(Math.floor(0.42 * (chars.length - 1)), chars.length - 1)]\n        }\n      }\n    }\n  }\n}\n\n// ============================================================================\n// Component factory\n// ============================================================================\n\ntype DrawFn = (grid: string[][], cols: number, rows: number, t: number, chars: string[], extra?: ColState[]) => void\n\nfunction makeAsciiComponent(drawFn: DrawFn, defaultCharset: AsciiCharset = 'classic') {\n  return React.forwardRef<HTMLPreElement, AsciiShapeProps>(\n    (\n      {\n        size = 'md',\n        charset = defaultCharset,\n        color,\n        speed = 'normal',\n        animated = true,\n        multicolor = false,\n        className,\n        ...props\n      },\n      ref\n    ) => {\n      const { cols, rows } = SIZE_MAP[size]\n      const chars = CHARSETS[charset]\n      const speedMul = SPEED_MAP[speed]\n      const matrixStateRef = React.useRef<ColState[]>(makeMatrixState(cols))\n      const preRef = React.useRef<HTMLPreElement | null>(null)\n      const spanRefs = React.useRef<(HTMLSpanElement | null)[]>([])\n\n      // Compute first frame for initial paint only — never triggers re-renders\n      const initialLines = React.useMemo(() => {\n        matrixStateRef.current = makeMatrixState(cols)\n        const g = makeGrid(cols, rows)\n        drawFn(g, cols, rows, 0, chars, matrixStateRef.current)\n        return gridToLines(g)\n      // eslint-disable-next-line react-hooks/exhaustive-deps\n      }, [cols, rows, chars])\n\n      React.useEffect(() => {\n        const pre = preRef.current\n        if (!pre) return\n\n        function writeLines(lines: string[]) {\n          if (multicolor) {\n            spanRefs.current.forEach((span, i) => {\n              if (span) span.textContent = lines[i] ?? ''\n            })\n          } else {\n            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n            pre!.textContent = lines.join('\\n')\n          }\n        }\n\n        if (!animated) {\n          writeLines(initialLines)\n          return\n        }\n\n        const startTime = performance.now()\n        let rafId: number\n\n        function frame(now: number) {\n          const t = (now - startTime) * speedMul\n          const g = makeGrid(cols, rows)\n          drawFn(g, cols, rows, t, chars, matrixStateRef.current)\n          writeLines(gridToLines(g))\n          rafId = requestAnimationFrame(frame)\n        }\n\n        rafId = requestAnimationFrame(frame)\n        return () => cancelAnimationFrame(rafId)\n      // eslint-disable-next-line react-hooks/exhaustive-deps\n      }, [size, charset, speed, animated, multicolor])\n\n      function combineRef(el: HTMLPreElement | null) {\n        preRef.current = el\n        if (typeof ref === 'function') ref(el)\n        else if (ref) (ref as React.MutableRefObject<HTMLPreElement | null>).current = el\n      }\n\n      return (\n        <pre\n          aria-hidden=\"true\"\n          ref={combineRef}\n          className={cn(\n            'inline-block border-3 border-foreground shadow-[4px_4px_0px_hsl(var(--shadow-color))] bg-background overflow-hidden',\n            'font-mono text-xs leading-none tracking-tight select-none p-1',\n            className\n          )}\n          style={multicolor ? undefined : { color: color || 'currentColor' }}\n          {...props}\n        >\n          {multicolor\n            ? initialLines.map((line, i) => (\n                <React.Fragment key={i}>\n                  <span\n                    ref={el => { spanRefs.current[i] = el }}\n                    style={{ color: MULTICOLOR_PALETTE[i % MULTICOLOR_PALETTE.length] }}\n                  >{line}</span>\n                  {i < initialLines.length - 1 && '\\n'}\n                </React.Fragment>\n              ))\n            : initialLines.join('\\n')}\n        </pre>\n      )\n    }\n  )\n}\n\n// ============================================================================\n// Named exports — 17 ASCII shape components\n// ============================================================================\n\nexport const AsciiSpiral = makeAsciiComponent(drawSpiral, 'classic')\nAsciiSpiral.displayName = 'AsciiSpiral'\n\nexport const AsciiRose = makeAsciiComponent(drawRose, 'braille')\nAsciiRose.displayName = 'AsciiRose'\n\nexport const AsciiWave = makeAsciiComponent(drawWave, 'classic')\nAsciiWave.displayName = 'AsciiWave'\n\nexport const AsciiVortex = makeAsciiComponent(drawVortex, 'blocks')\nAsciiVortex.displayName = 'AsciiVortex'\n\nexport const AsciiPulse = makeAsciiComponent(drawPulse, 'dots')\nAsciiPulse.displayName = 'AsciiPulse'\n\nexport const AsciiMatrix = makeAsciiComponent(\n  (g, cols, rows, t, chars, state) => drawMatrix(g, cols, rows, t, chars, state!), 'classic'\n)\nAsciiMatrix.displayName = 'AsciiMatrix'\n\nexport const AsciiGrid = makeAsciiComponent(drawGrid, 'line')\nAsciiGrid.displayName = 'AsciiGrid'\n\nexport const AsciiTorus = makeAsciiComponent(drawTorus, 'blocks')\nAsciiTorus.displayName = 'AsciiTorus'\n\nexport const AsciiSphere = makeAsciiComponent(drawSphere, 'classic')\nAsciiSphere.displayName = 'AsciiSphere'\n\nexport const AsciiCube = makeAsciiComponent(drawCube, 'blocks')\nAsciiCube.displayName = 'AsciiCube'\n\nexport const AsciiHelix = makeAsciiComponent(drawHelix, 'braille')\nAsciiHelix.displayName = 'AsciiHelix'\n\nexport const AsciiDonut = makeAsciiComponent(drawDonut, 'classic')\nAsciiDonut.displayName = 'AsciiDonut'\n\nexport const AsciiTrefoilKnot = makeAsciiComponent(drawTrefoilKnot, 'blocks')\nAsciiTrefoilKnot.displayName = 'AsciiTrefoilKnot'\n\nexport const AsciiGeodesicDome = makeAsciiComponent(drawGeodesicDome, 'classic')\nAsciiGeodesicDome.displayName = 'AsciiGeodesicDome'\n\nexport const AsciiSaturn = makeAsciiComponent(drawSaturn, 'blocks')\nAsciiSaturn.displayName = 'AsciiSaturn'\n\nexport const AsciiHyperboloid = makeAsciiComponent(drawHyperboloid, 'classic')\nAsciiHyperboloid.displayName = 'AsciiHyperboloid'\n\nexport const AsciiDNA = makeAsciiComponent(drawDNA, 'braille')\nAsciiDNA.displayName = 'AsciiDNA'\n",
      "type": "registry:ui",
      "target": "components/ui/ascii-shapes.tsx"
    }
  ],
  "type": "registry:ui"
}