{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "empty-state-braille-orbit",
  "title": "Empty State Braille Orbit",
  "description": "A small ambient icon for empty states: an unbroken stroke traced through braille's 2x4 sub-cell dot addressing along a closed 3:2 Lissajous orbit, sampled by arc length so the curve never gaps or clusters.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/empty-state-braille-orbit/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\n// ---------------------------------------------------------------------------\n// BrailleOrbit — a small ambient icon for empty states: one unbroken stroke\n// traced through Unicode braille's 2x4 sub-cell dot addressing along a closed\n// 3:2 Lissajous curve. Braille's real discriminating property over any other\n// density-ramp glyph is that 2x4 addressable sub-grid — a one-dot-wide curve\n// at 4x the resolution a single glyph-per-cell ramp can give. Distinct from\n// loader-braille, which spends the same dot grid on a 1D bar/progress fill (a\n// tone); this spends it on a 2D trajectory instead.\n//\n// Curve is sampled by ARC LENGTH, never a fixed time-step: each new point is\n// found by bisecting the step size in curve-parameter `t` until it lands\n// within MAX_ARC_DOT dot-units of the previous point, so no two consecutive\n// dots ever gap by more than one dot — fixed-t sampling would gap at high\n// curvature and cluster at low curvature. A ~140-sample ring buffer of the\n// most recent points carries the trail; each is mapped to its glyph cell and\n// sub-dot bit every frame, giving a comet read (bright head, fading tail)\n// rather than a fully-lit static loop.\n//\n// Head-to-tail fade is an ALPHA ramp on foreground-colored glyphs, never\n// arithmetic on the token color strings: every lit cell is drawn once in\n// --ns-muted at full alpha (the base), then again in --foreground at an\n// alpha equal to the freshest sample that touched it — near the head that\n// second pass is near-opaque and fully covers the base, near the tail it's\n// near-transparent and lets the muted base show through untouched. Works\n// unmodified if a token is ever redefined as non-grayscale.\n//\n// Amplitude eases 0->full once on mount over 1.2s ease-out-expo (the one\n// damped moment); after that the orbit is a steady, undamped, energy-\n// conserving loop that never settles or repeats within any short window\n// (period ~27s). Same offscreen-measureText/document.fonts.ready cell-metric\n// convention as the rest of the ascii family, tokens via getComputedStyle +\n// a MutationObserver on documentElement's class, one rAF loop paused on\n// visibilitychange. Purely decorative: aria-hidden, no accent, no\n// interaction.\n// ---------------------------------------------------------------------------\n\n// Unicode braille dot bit, indexed [subRow 0-3][subCol 0-1] within a cell's\n// 2x4 sub-grid (dot1=0x01 .. dot8=0x80, canonical U+2800 layout).\nconst DOT_BIT: readonly (readonly [number, number])[] = [\n  [0x01, 0x08],\n  [0x02, 0x10],\n  [0x04, 0x20],\n  [0x40, 0x80],\n];\n\nconst FREQ_X = 0.35; // rad/s\nconst FREQ_Y = 0.233; // rad/s — 0.35:0.233 gives the closed 3:2 Lissajous\nconst AMP_FRAC = 0.35; // amplitude as a fraction of the grid box dimension\nconst RAMP_S = 1.2; // amplitude ease-in duration, seconds\nconst RING_SIZE = 140;\nconst MAX_ARC_DOT = 0.6; // max dot-unit distance between consecutive samples\nconst MAX_EMITS_PER_FRAME = 80; // safety cap, not expected to bind at 60fps\nconst MAX_STEP_T = 0.5; // ceiling on the arc-march step guess, seconds\nconst DT_MAX = 0.05;\nconst REDUCED_ARC_DOTS = 90;\nconst REDUCED_START_T = 0.9; // clear of the axis crossing near t=0\n\nfunction easeOutExpo(x: number): number {\n  return x >= 1 ? 1 : 1 - Math.pow(2, -10 * x);\n}\n\nexport interface BrailleOrbitProps {\n  /** braille glyph cell size in px */\n  cellSize?: number;\n  /** icon box size in px (square) */\n  size?: number;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nexport function BrailleOrbit({ cellSize = 14, size = 120, className = \"\" }: BrailleOrbitProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n    let fg = \"currentColor\";\n    let muted = \"currentColor\";\n    let cellW = cellSize;\n    let cellH = cellSize;\n    let cols = 0;\n    let rows = 0;\n    let width = 0;\n    let height = 0;\n    let dpr = 1;\n    let sized = false;\n    let ready = false;\n    let disposed = false;\n\n    // curve geometry, set on resize\n    let cx = 0;\n    let cy = 0;\n    let ampX = 0;\n    let ampY = 0;\n    let dotPitchX = 1;\n    let dotPitchY = 1;\n    let dotCols = 0;\n    let dotRows = 0;\n\n    // per-cell scratch, rebuilt every frame from the ring buffer\n    let cellMask: Uint8Array = new Uint8Array(0);\n    let cellWeight: Float32Array = new Float32Array(0);\n\n    // ring buffer of trailing curve samples (px space, cheapest to store)\n    const ringX = new Float32Array(RING_SIZE);\n    const ringY = new Float32Array(RING_SIZE);\n    let ringHead = 0;\n    let ringCount = 0;\n\n    let hasEmitted = false;\n    let lastEmitT = 0;\n    let lastEmitX = 0;\n    let lastEmitY = 0;\n    let marchGuessDT = 0.25;\n\n    const readTokens = () => {\n      const s = getComputedStyle(document.documentElement);\n      fg = s.getPropertyValue(\"--foreground\").trim() || \"currentColor\";\n      muted = s.getPropertyValue(\"--ns-muted\").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    // position on the closed 3:2 Lissajous at curve-parameter t (seconds\n    // since the loop started); amplitude carries its own one-shot ease-in.\n    const scratch = { x: 0, y: 0 };\n    const pos = (t: number, out: { x: number; y: number }) => {\n      const frac = easeOutExpo(Math.min(1, t / RAMP_S));\n      out.x = cx + ampX * frac * Math.sin(FREQ_X * t);\n      out.y = cy + ampY * frac * Math.sin(FREQ_Y * t + Math.PI / 2);\n    };\n\n    const distDots = (x0: number, y0: number, x1: number, y1: number) => {\n      const dx = (x1 - x0) / dotPitchX;\n      const dy = (y1 - y0) / dotPitchY;\n      return Math.hypot(dx, dy);\n    };\n\n    // one arc-length-bounded step from (fromT, fromX, fromY): tries maxStepT,\n    // bisects down while the resulting dot-distance exceeds MAX_ARC_DOT.\n    const stepArc = (fromT: number, fromX: number, fromY: number, maxStepT: number) => {\n      let stepT = maxStepT;\n      let candT = fromT + stepT;\n      pos(candT, scratch);\n      let d = distDots(fromX, fromY, scratch.x, scratch.y);\n      let tries = 0;\n      while (d > MAX_ARC_DOT && tries < 24) {\n        stepT *= 0.5;\n        candT = fromT + stepT;\n        pos(candT, scratch);\n        d = distDots(fromX, fromY, scratch.x, scratch.y);\n        tries++;\n      }\n      return { t: candT, x: scratch.x, y: scratch.y, d, stepT };\n    };\n\n    const pushSample = (x: number, y: number) => {\n      ringX[ringHead] = x;\n      ringY[ringHead] = y;\n      ringHead = (ringHead + 1) % RING_SIZE;\n      if (ringCount < RING_SIZE) ringCount++;\n    };\n\n    // advances the live orbit head to simulation time targetT, emitting as\n    // many arc-length-bounded samples as needed to get there.\n    const advanceTo = (targetT: number) => {\n      if (!hasEmitted) {\n        pos(targetT, scratch);\n        pushSample(scratch.x, scratch.y);\n        lastEmitT = targetT;\n        lastEmitX = scratch.x;\n        lastEmitY = scratch.y;\n        hasEmitted = true;\n        marchGuessDT = 0.25;\n        return;\n      }\n      let emits = 0;\n      while (emits < MAX_EMITS_PER_FRAME) {\n        // the arc-length step is sized by curvature alone, never by how much\n        // real time this frame has left — clamping it to targetT-lastEmitT\n        // would force one sample per frame regardless of curve speed, which\n        // collapses the trail to a handful of frame-spaced points instead of\n        // a curve sampled evenly by arc length.\n        const r = stepArc(lastEmitT, lastEmitX, lastEmitY, marchGuessDT);\n        if (r.t > targetT) break;\n        pushSample(r.x, r.y);\n        lastEmitT = r.t;\n        lastEmitX = r.x;\n        lastEmitY = r.y;\n        marchGuessDT = r.d < MAX_ARC_DOT * 0.5 ? Math.min(MAX_STEP_T, r.stepT * 1.6) : r.stepT;\n        if (marchGuessDT < 1e-4) marchGuessDT = 1e-4;\n        emits++;\n      }\n    };\n\n    // maps one curve point to its glyph cell + sub-dot bit, ORing the bit\n    // into that cell's mask and keeping the freshest (max) weight touching it\n    const mapPointToCell = (x: number, y: number, weight: number) => {\n      let dotCol = Math.floor(x / dotPitchX);\n      let dotRow = Math.floor(y / dotPitchY);\n      if (dotCol < 0) dotCol = 0;\n      else if (dotCol >= dotCols) dotCol = dotCols - 1;\n      if (dotRow < 0) dotRow = 0;\n      else if (dotRow >= dotRows) dotRow = dotRows - 1;\n      const cellCol = dotCol >> 1;\n      const cellRow = dotRow >> 2;\n      const bit = DOT_BIT[dotRow & 3]![dotCol & 1]!;\n      const cellIdx = cellRow * cols + cellCol;\n      cellMask[cellIdx]! |= bit;\n      if (weight > cellWeight[cellIdx]!) cellWeight[cellIdx] = weight;\n    };\n\n    const buildCellsFromRing = () => {\n      cellMask.fill(0);\n      cellWeight.fill(0);\n      const n = ringCount;\n      for (let i = 0; i < n; i++) {\n        const idx = (ringHead - n + i + RING_SIZE * 2) % RING_SIZE;\n        const weight = n <= 1 ? 1 : Math.pow(i / (n - 1), 1.3);\n        mapPointToCell(ringX[idx]!, ringY[idx]!, weight);\n      }\n    };\n\n    const draw = () => {\n      if (!sized) return;\n      ctx.clearRect(0, 0, width, height);\n      // base pass: muted ink at full alpha under every lit cell\n      ctx.globalAlpha = 1;\n      ctx.fillStyle = muted;\n      for (let r = 0; r < rows; r++) {\n        for (let c = 0; c < cols; c++) {\n          const idx = r * cols + c;\n          const mask = cellMask[idx]!;\n          if (!mask) continue;\n          ctx.fillText(String.fromCharCode(0x2800 + mask), c * cellW + cellW / 2, r * cellH + cellH / 2);\n        }\n      }\n      // overlay pass: foreground crossfaded in by per-cell alpha weight —\n      // the fade itself, never color-string arithmetic\n      ctx.fillStyle = fg;\n      for (let r = 0; r < rows; r++) {\n        for (let c = 0; c < cols; c++) {\n          const idx = r * cols + c;\n          const mask = cellMask[idx]!;\n          if (!mask) continue;\n          const w = cellWeight[idx]!;\n          if (w <= 0.02) continue;\n          ctx.globalAlpha = w;\n          ctx.fillText(String.fromCharCode(0x2800 + mask), c * cellW + cellW / 2, r * cellH + cellH / 2);\n        }\n      }\n      ctx.globalAlpha = 1;\n    };\n\n    // reduced motion: one baked ~90-dot curved arc, no ring buffer, no rAF.\n    // Starts past REDUCED_START_T so the frozen segment sits on a genuinely\n    // curved stretch rather than the near-straight run through an axis\n    // crossing, which would read as a dead line, not an orbit fragment.\n    const drawReducedStatic = () => {\n      if (!sized) return;\n      pos(REDUCED_START_T, scratch);\n      let t = REDUCED_START_T;\n      let x = scratch.x;\n      let y = scratch.y;\n      const ptsX: number[] = [x];\n      const ptsY: number[] = [y];\n      let guessDT = 0.25;\n      while (ptsX.length < REDUCED_ARC_DOTS) {\n        const r = stepArc(t, x, y, guessDT);\n        t = r.t;\n        x = r.x;\n        y = r.y;\n        ptsX.push(x);\n        ptsY.push(y);\n        guessDT = r.d < MAX_ARC_DOT * 0.5 ? r.stepT * 1.6 : r.stepT;\n        if (guessDT < 1e-4) guessDT = 1e-4;\n      }\n      cellMask.fill(0);\n      cellWeight.fill(0);\n      const n = ptsX.length;\n      for (let i = 0; i < n; i++) {\n        mapPointToCell(ptsX[i]!, ptsY[i]!, Math.pow(i / (n - 1), 1.3));\n      }\n      draw();\n    };\n\n    const resetOrbit = () => {\n      ringHead = 0;\n      ringCount = 0;\n      hasEmitted = false;\n    };\n\n    const resize = () => {\n      const rect = canvas.getBoundingClientRect();\n      width = rect.width;\n      height = rect.height;\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(4, Math.round(width / cellW));\n      rows = Math.max(4, Math.round(height / cellH));\n      dotPitchX = cellW / 2;\n      dotPitchY = cellH / 4;\n      dotCols = cols * 2;\n      dotRows = rows * 4;\n      cx = (cols * cellW) / 2;\n      cy = (rows * cellH) / 2;\n      ampX = AMP_FRAC * cols * cellW;\n      ampY = AMP_FRAC * rows * cellH;\n      cellMask = new Uint8Array(cols * rows);\n      cellWeight = new Float32Array(cols * rows);\n      resetOrbit();\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) drawReducedStatic();\n      }, 150);\n    };\n\n    // -- hot-path state -------------------------------------------------------\n    let raf = 0;\n    let last = 0;\n    let simT = 0;\n\n    const loop = (now: number) => {\n      const dt = last ? Math.min(DT_MAX, (now - last) / 1000) : 1 / 60;\n      last = now;\n      simT += dt;\n      advanceTo(simT);\n      buildCellsFromRing();\n      draw();\n      if (!document.hidden) raf = requestAnimationFrame(loop);\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();\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        drawReducedStatic();\n      } else {\n        raf = requestAnimationFrame(loop);\n      }\n    });\n\n    window.addEventListener(\"resize\", onResize);\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      document.removeEventListener(\"visibilitychange\", onVis);\n    };\n  }, [cellSize, size]);\n\n  return (\n    <canvas\n      ref={canvasRef}\n      aria-hidden=\"true\"\n      style={{ width: size, height: size }}\n      className={`block font-mono ${className}`}\n    />\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/empty-state-braille-orbit.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)"
    },
    "light": {
      "ns-muted": "#4d4d4d"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "empty-state",
      "icon",
      "ascii",
      "braille",
      "mono",
      "ambient"
    ],
    "instruction": "A small ambient icon for empty states: a single unbroken stroke traced through Unicode braille's 2x4 sub-cell dot addressing (U+2800 base + bitmask) along a closed 3:2 Lissajous curve, x = A*sin(0.35t), y = B*sin(0.233t + PI/2), giving each glyph cell an addressable 2-wide/4-tall dot sub-grid. The curve is sampled by ARC LENGTH, never a fixed time-step: each new point is found by bisecting the step in curve-parameter t until it lands within 0.6 dot-units of the previous point, guaranteeing no two consecutive dots ever gap by more than one dot — fixed-t sampling would gap at high curvature and cluster at low curvature. A ~140-sample ring buffer of the most recent curve points is remapped to glyph cells and sub-dot bits every frame, giving a slow comet-orbit read (bright head, fading tail) distinct from loader-braille's bar/dot progress fill, which spends the same addressable grid on a 1D tone instead of a 2D trajectory. The head-to-tail fade is an alpha ramp, not color arithmetic: every lit cell is drawn once in --ns-muted at full alpha, then again in --foreground at an alpha equal to the freshest sample touching it, so the two-pass crossfade works unmodified even if a token is ever redefined as non-grayscale. Amplitude eases 0-100% over 1.2s ease-out-expo once on mount; after that the orbit is a steady, undamped, energy-conserving loop with a ~27s period that never settles or stops. Cell metrics come from an offscreen measureText call after document.fonts.ready, and both tokens are re-read via getComputedStyle on a MutationObserver watching documentElement's class, so both themes render correctly without a remount. prefers-reduced-motion freezes on one baked ~90-dot curved arc (starting past the axis crossing near t=0, so the frozen segment reads as a legible orbit fragment rather than a straight dead line) — no ring buffer, no rAF. Purely decorative: the canvas is aria-hidden; the empty state's real accessible content (a heading and description) lives as ordinary DOM alongside it, never encoded in the glyph art. Props: cellSize (default 14, braille glyph cell px), size (default 120, icon box px, square), className."
  },
  "type": "registry:ui"
}