{
  "name": "grid-overlay",
  "type": "registry:ui",
  "title": "GridOverlay",
  "description": "The baseline grid made visible (§2.6a): minor line every 4px baseline, major every body leading. Governance, not chrome — a grid you can't toggle on and measure is a mood board.",
  "dependencies": [
    "@candlelight-ds/tokens",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "components/ui/GridOverlay.tsx",
      "type": "registry:ui",
      "content": "'use client'\n\n/**\n * GridOverlay — the baseline grid made visible AND measured (§2.6a).\n * A grid you can't toggle on and measure is a mood board: this draws\n * one line per baseline unit (one weight — the unit is the law), then\n * walks the audited surface and marks every text element whose top\n * doesn't sit on the rhythm with the danger outline. Verification\n * accuses; alignment stays quiet.\n *\n * ⚠️ TRAP (learned upstream — do not re-learn): the overlay MUST be\n * mounted INSIDE the surface it audits — the same content box, a\n * `relative` parent — never as a full-width sibling. An overlay in a\n * different box drifts from the content at some viewport width and\n * lies about alignment. The measuring pass inherits the same rule: it\n * measures against the overlay's own box.\n *\n * Toggle it from your surface map (the G key by convention) so the\n * binding prints in the generated help (§5).\n *\n * Use when: proving a surface's vertical rhythm during review.\n * Don't use when: decorating. It ships behind a dev-only or\n * review-only gate; the grid is governance, not chrome.\n */\n\nimport { useEffect, useRef } from 'react'\nimport { cn } from '@/lib/utils'\n\ninterface GridOverlayProps {\n  /** Render the lines (bind the G key in your surface map). */\n  open: boolean\n  /** Mark off-baseline text with the danger outline (default on). */\n  measure?: boolean\n  /** Draw the horizontal rhythm too — spacing shares the unit. */\n  square?: boolean\n  className?: string\n}\n\nexport function GridOverlay({ open, measure = true, square = false, className }: GridOverlayProps) {\n  const ref = useRef<HTMLDivElement>(null)\n\n  // The measuring pass: every leaf text element's top, relative to the\n  // overlay's own box (= the audited content box), must sit on the\n  // baseline. Marks are cleaned up when the overlay closes.\n  useEffect(() => {\n    const overlay = ref.current\n    if (!open || !measure || !overlay) return\n    const surface = overlay.parentElement\n    if (!surface) return\n\n    const baseline =\n      parseFloat(getComputedStyle(overlay).getPropertyValue('--baseline')) || 4\n    const origin = overlay.getBoundingClientRect().top\n    const marked: HTMLElement[] = []\n    // Scope (§2.6a): the law binds the FLOW axis. Control interiors\n    // (buttons, keys, tabs, inputs) and fixed bands center optically —\n    // modules, not violations — so their text is exempt; opt anything\n    // else out with data-optical on an ancestor. The unit measured is\n    // the TEXT BLOCK (an element whose children are all inline): an\n    // inline rect sits at font-metric positions, and items-baseline\n    // rows share a baseline by design — the block's box top is what\n    // the rhythm binds.\n    const OPTICAL = 'button, [role=\"button\"], [role=\"tab\"], kbd, input, select, textarea, [data-optical], [data-candlelight-console]'\n    const isTextBlock = (el: HTMLElement) => {\n      if (!el.textContent?.trim()) return false\n      if (getComputedStyle(el).display === 'inline') return false\n      // items-baseline rows share a BASELINE by design: mixed-size box\n      // tops legitimately differ; the row's block carries the position.\n      if (el.parentElement && getComputedStyle(el.parentElement).alignItems === 'baseline')\n        return false\n      return [...el.children].every((c) => getComputedStyle(c).display.startsWith('inline'))\n    }\n    surface.querySelectorAll<HTMLElement>('*').forEach((el) => {\n      if (el === overlay || overlay.contains(el)) return\n      if (el.closest(OPTICAL) || !isTextBlock(el)) return\n      const offset = el.getBoundingClientRect().top - origin\n      const off = ((offset % baseline) + baseline) % baseline\n      if (off > 0.5 && off < baseline - 0.5) {\n        el.dataset.offBaseline = ''\n        marked.push(el)\n      }\n    })\n    return () => {\n      for (const el of marked) delete el.dataset.offBaseline\n    }\n  }, [open, measure])\n\n  if (!open) return null\n  return (\n    <div\n      ref={ref}\n      aria-hidden\n      data-square={square ? '' : undefined}\n      className={cn('baseline-overlay z-announce', className)}\n    />\n  )\n}\n",
      "target": "components/ui/GridOverlay.tsx"
    }
  ],
  "meta": {
    "candlelight": {
      "category": "primitive",
      "gRules": [],
      "useWhen": "Proving a surface's vertical rhythm in review — every leading, control height, and band metric on the baseline.",
      "dontUseWhen": "Decorating. Ship it behind a dev/review gate; the grid is governance, not chrome.",
      "states": [
        "hidden",
        "open"
      ],
      "keyboard": [
        {
          "keys": "G",
          "does": "toggle the overlay (bind it in your surface map so it prints in the help)"
        }
      ],
      "traps": [
        "Mount the overlay INSIDE the surface it audits — same content box, a relative parent — never as a full-width sibling. An overlay in a different box drifts from the content at some viewport width and lies about alignment."
      ],
      "acceptanceTests": [
        "Every text step's bundled line-height is a whole multiple of --baseline (the tokens package law-tests this).",
        "The overlay renders nothing when closed and is aria-hidden + pointer-events-none when open."
      ]
    }
  }
}