{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "compare-table-reach-rule",
  "title": "Compare Table Reach Rule",
  "description": "A tier comparison matrix where every feature row draws a real box-drawing rule that runs left-to-right and terminates with ┤ at the last tier that actually includes it, so the table's staircase of rule lengths is the comparison itself rather than a field of check glyphs.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/compare-table-reach-rule/component.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\n\n// ---------------------------------------------------------------------------\n// ReachRuleTable — a tier comparison matrix where each feature row draws a real\n// box-drawing rule that starts at the x-centre of the first tier column and\n// terminates with ┤ on the x-centre of the LAST tier that includes it. Columns\n// the feature skips are drawn ┄ rather than ─, so a non-monotone matrix is\n// shown honestly instead of smoothed. The staircase of rule lengths is the\n// comparison — there is no field of check glyphs to scan.\n// ---------------------------------------------------------------------------\n\nexport type CompareTier = {\n  id: string;\n  /** tier name, e.g. \"Pro\" */\n  name: string;\n  /** price string, rendered verbatim — e.g. \"$20/mo\" or \"Custom\" */\n  price: string;\n};\n\nexport type CompareFeatureRow = {\n  id: string;\n  /** feature name — becomes the row's pin button label */\n  label: string;\n  /** optional second line under the label */\n  note?: string;\n  /** one entry per tier, in tier order. `false` = not included, a string = the\n   *  tier-specific value (\"14 days\"), `true` = plainly included */\n  support: (boolean | string)[];\n};\n\nconst STEP_IN_MS = 14; // per glyph cell, drawing\nconst STEP_OUT_MS = 9; // per glyph cell, retracting\nconst MAX_IN_MS = 320; // total draw budget — long rules step faster, never slower\nconst REST_OPACITY = 0.42; // resting rule ink — the idle staircase must read\nconst DIM_OPACITY = 0.35; // tiers excluded by the active row\nconst TRACK_CELLS = 12; // completeness readout width\nconst STACK_BREAKPOINT = 720; // px — below this the matrix becomes stacked cards\nconst WIDTH_EPSILON = 2; // px — remeasure only past this much layout change\n\nconst GLYPH_RUN = \"─\";\nconst GLYPH_GAP = \"┄\";\nconst GLYPH_END = \"┤\";\n\nconst DEFAULT_TIERS: CompareTier[] = [\n  { id: \"hobby\", name: \"Hobby\", price: \"$0\" },\n  { id: \"pro\", name: \"Pro\", price: \"$20/mo\" },\n  { id: \"team\", name: \"Team\", price: \"$60/mo\" },\n  { id: \"enterprise\", name: \"Enterprise\", price: \"Custom\" },\n];\n\n// Ordered by reach, so the rules descend as a staircase at rest.\nconst DEFAULT_ROWS: CompareFeatureRow[] = [\n  {\n    id: \"deployments\",\n    label: \"Unlimited deployments\",\n    support: [true, true, true, true],\n  },\n  {\n    id: \"regions\",\n    label: \"Edge regions\",\n    note: \"Where builds are served from\",\n    support: [\"3\", \"18\", \"34\", \"34 + private\"],\n  },\n  {\n    id: \"analytics\",\n    label: \"Traffic analytics\",\n    note: \"Retention window\",\n    support: [false, \"30 days\", \"1 year\", \"3 years\"],\n  },\n  {\n    id: \"seats\",\n    label: \"Collaborator seats\",\n    support: [\"1\", \"5\", \"25\", \"Unlimited\"],\n  },\n  {\n    id: \"sso\",\n    label: \"SAML single sign-on\",\n    support: [false, false, true, true],\n  },\n  {\n    id: \"audit\",\n    label: \"Audit log export\",\n    note: \"Streamed to your own bucket\",\n    support: [false, false, false, true],\n  },\n  {\n    id: \"checkout\",\n    label: \"Self-serve checkout\",\n    note: \"Enterprise is invoiced instead\",\n    support: [true, true, true, false],\n  },\n  {\n    id: \"overage\",\n    label: \"Usage overage on card\",\n    support: [true, true, true, false],\n  },\n  {\n    id: \"community\",\n    label: \"Community forum support\",\n    note: \"Superseded by a named contact on Enterprise\",\n    support: [true, true, true, false],\n  },\n  {\n    id: \"trial\",\n    label: \"14-day trial without a card\",\n    support: [true, true, false, false],\n  },\n  {\n    id: \"sandbox\",\n    label: \"Shared sandbox runners\",\n    note: \"Dedicated runners replace these higher up\",\n    support: [true, true, false, false],\n  },\n  {\n    id: \"badge\",\n    label: \"Public project badge\",\n    note: \"Free-tier attribution\",\n    support: [true, false, false, false],\n  },\n];\n\nconst has = (v: boolean | string | undefined): boolean =>\n  v === true || (typeof v === \"string\" && v.trim().length > 0);\n\n/** highest tier index this feature still appears in, −1 if none */\nfunction reachOf(row: CompareFeatureRow, n: number): number {\n  for (let i = n - 1; i >= 0; i--) if (has(row.support[i])) return i;\n  return -1;\n}\n\ntype Geom = { text: string; left: number; width: number; len: number };\ntype Anim = { dir: 1 | -1; k: number };\n\nexport function ReachRuleTable({\n  tiers = DEFAULT_TIERS,\n  rows = DEFAULT_ROWS,\n  title = \"Plan comparison\",\n  className = \"\",\n}: {\n  /** tier columns, cheapest first */\n  tiers?: CompareTier[];\n  /** feature rows, one `support` entry per tier */\n  rows?: CompareFeatureRow[];\n  /** accessible name for the matrix */\n  title?: string;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}) {\n  const n = tiers.length;\n\n  const [activeId, setActiveId] = useState<string | null>(null);\n  const [pinnedId, setPinnedId] = useState<string | null>(null);\n  const [stacked, setStacked] = useState(false);\n  const [reduced, setReduced] = useState(false);\n\n  const rootRef = useRef<HTMLDivElement | null>(null);\n  const headRowRef = useRef<HTMLDivElement | null>(null);\n  const headCellRefs = useRef<(HTMLDivElement | null)[]>([]);\n  const probeRef = useRef<HTMLSpanElement | null>(null);\n  const ruleRefs = useRef<Map<string, HTMLSpanElement>>(new Map());\n\n  const geomRef = useRef<Map<string, Geom>>(new Map());\n  const animRef = useRef<Map<string, Anim>>(new Map());\n  const rafRef = useRef(0);\n  const lastWidthRef = useRef(-1);\n  const pinnedRef = useRef<string | null>(null);\n  pinnedRef.current = pinnedId;\n  const activeRef = useRef<string | null>(null);\n  activeRef.current = activeId;\n\n  const reaches = useMemo(\n    () => rows.map((r) => reachOf(r, n)),\n    [rows, n]\n  );\n\n  // completeness per tier: how many features that tier actually includes\n  const included = useMemo(\n    () => tiers.map((_, i) => rows.reduce((a, r) => a + (has(r.support[i]) ? 1 : 0), 0)),\n    [tiers, rows]\n  );\n\n  const gridStyle = useMemo(\n    () => ({\n      gridTemplateColumns: `minmax(12rem,1.4fr) repeat(${n}, minmax(6rem,1fr))`,\n    }),\n    [n]\n  );\n\n  useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const sync = () => setReduced(mq.matches);\n    sync();\n    mq.addEventListener(\"change\", sync);\n    return () => mq.removeEventListener(\"change\", sync);\n  }, []);\n\n  /** body glyphs + the ┤ cap live in two child spans, so the terminator can\n   *  take accent ink on a pinned row while the run stays --foreground */\n  const parts = (id: string) => {\n    const el = ruleRefs.current.get(id);\n    if (!el) return null;\n    const body = el.firstElementChild as HTMLElement | null;\n    const cap = el.lastElementChild as HTMLElement | null;\n    if (!body || !cap || body === cap) return null;\n    return { el, body, cap };\n  };\n\n  /** paint one rule at a given glyph count */\n  const paint = useCallback((id: string, k: number) => {\n    const p = parts(id);\n    const g = geomRef.current.get(id);\n    if (!p || !g) return;\n    const shown = Math.max(0, Math.min(g.len, Math.round(k)));\n    p.body.textContent = shown > 0 ? g.text.slice(0, shown - 1) : \"\";\n    p.cap.textContent = shown > 0 ? GLYPH_END : \"\";\n    p.cap.style.color =\n      pinnedRef.current === id ? \"var(--ns-accent)\" : \"currentColor\";\n    const t = g.len > 0 ? shown / g.len : 1;\n    p.el.style.opacity = String(REST_OPACITY + (1 - REST_OPACITY) * t);\n  }, []);\n\n  const settle = useCallback((id: string, lit: boolean) => {\n    const p = parts(id);\n    const g = geomRef.current.get(id);\n    if (!p || !g) return;\n    p.body.textContent = g.text.slice(0, g.len - 1);\n    p.cap.textContent = GLYPH_END;\n    p.cap.style.color =\n      pinnedRef.current === id ? \"var(--ns-accent)\" : \"currentColor\";\n    p.el.style.opacity = String(lit ? 1 : REST_OPACITY);\n  }, []);\n\n  /** single shared rAF stepping every in-flight rule */\n  const tick = useCallback(\n    (now: number, prev: number) => {\n      const dt = Math.min(64, now - prev);\n      const anims = animRef.current;\n      for (const [id, a] of anims) {\n        const g = geomRef.current.get(id);\n        if (!g) {\n          anims.delete(id);\n          continue;\n        }\n        const len = Math.max(1, g.len);\n        const stepMs =\n          a.dir === 1 ? Math.min(STEP_IN_MS, MAX_IN_MS / len) : STEP_OUT_MS;\n        a.k += (dt / stepMs) * a.dir;\n        if (a.dir === 1 && a.k >= g.len) {\n          settle(id, true);\n          anims.delete(id);\n        } else if (a.dir === -1 && a.k <= 0) {\n          settle(id, pinnedRef.current === id);\n          anims.delete(id);\n        } else {\n          paint(id, a.k);\n        }\n      }\n    },\n    [paint, settle]\n  );\n\n  const pump = useCallback(() => {\n    if (rafRef.current) return;\n    let prev = performance.now();\n    const loop = (now: number) => {\n      tick(now, prev);\n      prev = now;\n      if (animRef.current.size > 0) {\n        rafRef.current = requestAnimationFrame(loop);\n      } else {\n        rafRef.current = 0;\n      }\n    };\n    rafRef.current = requestAnimationFrame(loop);\n  }, [tick]);\n\n  const drive = useCallback(\n    (id: string, dir: 1 | -1) => {\n      const g = geomRef.current.get(id);\n      if (!g) return;\n      if (reduced) {\n        animRef.current.delete(id);\n        settle(id, dir === 1 || pinnedRef.current === id);\n        return;\n      }\n      const cur = animRef.current.get(id);\n      const k = dir === 1 ? 0 : cur ? cur.k : g.len;\n      animRef.current.set(id, { dir, k });\n      if (dir === 1) paint(id, 0);\n      pump();\n    },\n    [paint, pump, reduced, settle]\n  );\n\n  /** one measurement pass: header centres → per-row rule text + box */\n  const layout = useCallback(\n    (force: boolean) => {\n      const root = rootRef.current;\n      const headRow = headRowRef.current;\n      if (!root) return;\n\n      const w = root.clientWidth;\n      if (w < 2) return;\n      if (!force && Math.abs(w - lastWidthRef.current) <= WIDTH_EPSILON) return;\n      lastWidthRef.current = w;\n\n      const nextStacked = w < STACK_BREAKPOINT;\n      setStacked(nextStacked);\n      if (nextStacked || !headRow) return;\n\n      const probe = probeRef.current;\n      const chW = probe ? probe.getBoundingClientRect().width / 40 : 8;\n      if (!(chW > 0.5)) return;\n\n      const rowLeft = headRow.getBoundingClientRect().left;\n      const centres: number[] = [];\n      for (let i = 0; i < n; i++) {\n        const cell = headCellRefs.current[i];\n        if (!cell) return;\n        const r = cell.getBoundingClientRect();\n        centres.push(r.left + r.width / 2 - rowLeft);\n      }\n\n      rows.forEach((row, ri) => {\n        const reach = reaches[ri];\n        const el = ruleRefs.current.get(row.id);\n        if (reach < 0) {\n          geomRef.current.delete(row.id);\n          const p = el ? parts(row.id) : null;\n          if (p) {\n            p.body.textContent = \"\";\n            p.cap.textContent = \"\";\n          }\n          return;\n        }\n        const left = centres[0];\n        const width = Math.max(0, centres[reach] - left);\n        const cells = Math.max(0, Math.round(width / chW));\n        let text = \"\";\n        for (let j = 0; j < cells; j++) {\n          const x = left + j * chW;\n          // nearest tier column owns this glyph\n          let col = 0;\n          let best = Infinity;\n          for (let i = 0; i <= reach; i++) {\n            const d = Math.abs(centres[i] - x);\n            if (d < best) {\n              best = d;\n              col = i;\n            }\n          }\n          text += has(row.support[col]) ? GLYPH_RUN : GLYPH_GAP;\n        }\n        text += GLYPH_END;\n        const g: Geom = {\n          text,\n          left: left - chW / 2,\n          width: width + chW,\n          len: text.length,\n        };\n        geomRef.current.set(row.id, g);\n        if (el) {\n          el.style.left = `${g.left}px`;\n          el.style.width = `${g.width}px`;\n          if (!animRef.current.has(row.id)) {\n            settle(row.id, pinnedRef.current === row.id);\n          }\n        }\n      });\n    },\n    [n, reaches, rows, settle]\n  );\n\n  useEffect(() => {\n    layout(true);\n    let alive = true;\n    // the 1ch probe is measured in whatever font is resolved at mount; when the\n    // mono face swaps in the pitch changes, so re-measure once fonts settle or\n    // every ┤ lands off its column centre\n    if (typeof document !== \"undefined\" && document.fonts) {\n      document.fonts.ready.then(() => {\n        if (alive) layout(true);\n      });\n    }\n    const root = rootRef.current;\n    if (!root) {\n      return () => {\n        alive = false;\n      };\n    }\n    const ro = new ResizeObserver(() => layout(false));\n    ro.observe(root);\n    return () => {\n      alive = false;\n      ro.disconnect();\n    };\n  }, [layout]);\n\n  // crossing back above the breakpoint remounts the matrix — the header cells\n  // only exist after that paint, so measure once more on the flip\n  useEffect(() => {\n    if (stacked) return;\n    const id = requestAnimationFrame(() => layout(true));\n    return () => cancelAnimationFrame(id);\n  }, [stacked, layout]);\n\n  useEffect(\n    () => () => {\n      if (rafRef.current) cancelAnimationFrame(rafRef.current);\n      rafRef.current = 0;\n      animRef.current.clear();\n    },\n    []\n  );\n\n  const enter = (id: string) => {\n    activeRef.current = id;\n    setActiveId(id);\n    drive(id, 1);\n  };\n  const leave = (id: string) => {\n    if (activeRef.current === id) activeRef.current = null;\n    setActiveId((cur) => (cur === id ? null : cur));\n    if (pinnedRef.current === id) {\n      animRef.current.delete(id);\n      settle(id, true);\n      return;\n    }\n    drive(id, -1);\n  };\n  // side effects live in the handler, never inside the state updater — a\n  // StrictMode double-invoke would otherwise paint twice\n  const togglePin = (id: string) => {\n    const prev = pinnedRef.current;\n    const next = prev === id ? null : id;\n    pinnedRef.current = next;\n    setPinnedId(next);\n    if (prev && prev !== next) settle(prev, activeRef.current === prev);\n    if (next) settle(next, true);\n  };\n\n  const activeRow = activeId ? rows.find((r) => r.id === activeId) : undefined;\n  const shownRow = activeRow ?? (pinnedId ? rows.find((r) => r.id === pinnedId) : undefined);\n\n  const track = (i: number) => {\n    const filled = Math.round((TRACK_CELLS * included[i]) / Math.max(1, rows.length));\n    return \"▪\".repeat(filled) + \"·\".repeat(Math.max(0, TRACK_CELLS - filled));\n  };\n\n  return (\n    <div\n      ref={rootRef}\n      className={`w-full text-foreground ${className}`.trim()}\n    >\n      {/* mono advance probe — measured, never seen */}\n      <span\n        ref={probeRef}\n        aria-hidden\n        className=\"pointer-events-none absolute -left-[9999px] top-0 font-mono text-[13px] leading-none\"\n        style={{ whiteSpace: \"pre\", letterSpacing: 0 }}\n      >\n        {GLYPH_RUN.repeat(40)}\n      </span>\n\n      {stacked ? (\n        <div className=\"flex flex-col gap-3\">\n          {tiers.map((tier, i) => (\n            <section\n              key={tier.id}\n              aria-label={`${tier.name} — ${tier.price}`}\n              className=\"rounded-md border border-border bg-surface p-4\"\n            >\n              <div className=\"flex items-baseline justify-between\">\n                <h3 className=\"text-sm font-medium\">{tier.name}</h3>\n                <span className=\"font-mono text-xs text-ns-muted\">{tier.price}</span>\n              </div>\n              <p className=\"mt-2 font-mono text-[11px] text-ns-muted\">\n                <span aria-hidden style={{ letterSpacing: \"0.12em\" }}>\n                  {track(i)}\n                </span>\n                <span className=\"ml-2\">\n                  {included[i]}/{rows.length}\n                </span>\n              </p>\n              <ul className=\"mt-3 flex flex-col gap-1.5\">\n                {rows.map((row) => {\n                  const on = has(row.support[i]);\n                  const val = typeof row.support[i] === \"string\" ? (row.support[i] as string) : null;\n                  return (\n                    <li\n                      key={row.id}\n                      className={`flex items-baseline justify-between gap-3 text-[13px] ${\n                        on ? \"text-foreground\" : \"text-ns-muted\"\n                      }`}\n                    >\n                      <span className=\"flex items-baseline gap-2\">\n                        <span aria-hidden className=\"font-mono text-[11px]\">\n                          {on ? \"●\" : \"○\"}\n                        </span>\n                        <span>{row.label}</span>\n                        <span className=\"sr-only\">\n                          {on ? \"included\" : \"not included\"}\n                        </span>\n                      </span>\n                      {val ? (\n                        <span className=\"font-mono text-[11px] text-ns-muted\">{val}</span>\n                      ) : null}\n                    </li>\n                  );\n                })}\n              </ul>\n            </section>\n          ))}\n        </div>\n      ) : (\n        <div\n          role=\"table\"\n          aria-label={title}\n          className=\"overflow-hidden rounded-md border border-border bg-surface\"\n        >\n          {/* header */}\n          <div\n            ref={headRowRef}\n            role=\"row\"\n            className=\"grid items-end gap-x-2 border-b border-border px-4 pb-3 pt-4\"\n            style={gridStyle}\n          >\n            <div role=\"columnheader\" className=\"font-mono text-[11px] uppercase tracking-[0.18em] text-ns-muted\">\n              Feature\n            </div>\n            {tiers.map((tier, i) => {\n              const dim = shownRow ? !has(shownRow.support[i]) : false;\n              return (\n                <div\n                  key={tier.id}\n                  role=\"columnheader\"\n                  ref={(el) => {\n                    headCellRefs.current[i] = el;\n                  }}\n                  className=\"text-center transition-opacity duration-200\"\n                  style={{ opacity: dim ? DIM_OPACITY : 1 }}\n                >\n                  <div className=\"text-sm font-medium\">{tier.name}</div>\n                  <div className=\"font-mono text-[11px] text-ns-muted\">{tier.price}</div>\n                  <div className=\"mt-1.5 font-mono text-[10px] text-ns-muted\">\n                    <span aria-hidden style={{ letterSpacing: \"0.1em\" }}>\n                      {track(i)}\n                    </span>\n                    <span className=\"ml-1.5\">\n                      {included[i]}/{rows.length}\n                    </span>\n                  </div>\n                </div>\n              );\n            })}\n          </div>\n\n          {/* feature rows */}\n          {rows.map((row, ri) => {\n            const isActive = activeId === row.id;\n            const isPinned = pinnedId === row.id;\n            const reach = reaches[ri];\n            return (\n              <div\n                key={row.id}\n                role=\"row\"\n                className=\"relative grid items-start gap-x-2 border-b border-border/60 px-4 py-2.5 last:border-b-0\"\n                style={gridStyle}\n              >\n                <div role=\"rowheader\" className=\"min-w-0\">\n                  <button\n                    type=\"button\"\n                    data-compare-row={row.id}\n                    aria-pressed={isPinned}\n                    onPointerEnter={() => enter(row.id)}\n                    onPointerLeave={() => leave(row.id)}\n                    onFocus={() => enter(row.id)}\n                    onBlur={() => leave(row.id)}\n                    onClick={() => togglePin(row.id)}\n                    className={`-mx-1 block w-full rounded-sm px-1 text-left text-[13px] leading-5 transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent ${\n                      isActive || isPinned ? \"text-foreground\" : \"text-foreground/80\"\n                    }`}\n                  >\n                    {row.label}\n                    {isPinned ? (\n                      <span className=\"sr-only\"> (pinned)</span>\n                    ) : null}\n                  </button>\n                  {row.note ? (\n                    <p className=\"mt-0.5 px-0 text-[11px] leading-4 text-ns-muted\">\n                      {row.note}\n                    </p>\n                  ) : null}\n                </div>\n\n                {tiers.map((tier, i) => {\n                  const raw = row.support[i];\n                  const on = has(raw);\n                  const dim = (isActive || (!activeId && isPinned)) && !on;\n                  return (\n                    <div\n                      key={tier.id}\n                      role=\"cell\"\n                      className=\"text-center transition-opacity duration-200\"\n                      style={{ opacity: dim ? DIM_OPACITY : 1 }}\n                    >\n                      {/* the rule seats on the row's first line; values hang below it */}\n                      <span aria-hidden className=\"block h-5\" />\n                      {typeof raw === \"string\" ? (\n                        <span className=\"block font-mono text-[11px] leading-4 text-ns-muted\">\n                          {raw}\n                        </span>\n                      ) : null}\n                      <span className=\"sr-only\">\n                        {on\n                          ? typeof raw === \"string\"\n                            ? raw\n                            : \"included\"\n                          : \"not included\"}\n                      </span>\n                    </div>\n                  );\n                })}\n\n                {reach >= 0 ? (\n                  <span\n                    aria-hidden\n                    ref={(el) => {\n                      if (el) ruleRefs.current.set(row.id, el);\n                      else ruleRefs.current.delete(row.id);\n                    }}\n                    data-reach-rule={row.id}\n                    className=\"pointer-events-none absolute font-mono text-[13px] leading-none\"\n                    style={{\n                      top: \"1.24rem\",\n                      transform: \"translateY(-50%)\",\n                      whiteSpace: \"pre\",\n                      letterSpacing: 0,\n                      overflow: \"hidden\",\n                      opacity: REST_OPACITY,\n                      color: \"var(--foreground)\",\n                    }}\n                  >\n                    <span />\n                    <span />\n                  </span>\n                ) : null}\n              </div>\n            );\n          })}\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport default ReachRuleTable;\n",
      "type": "registry:ui",
      "target": "components/ui/compare-table-reach-rule.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)",
      "color-surface": "var(--surface)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff",
      "surface": "#fafafa"
    },
    "dark": {
      "ns-muted": "#8f8f8f",
      "surface": "#171717"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "comparison",
      "table",
      "pricing",
      "matrix",
      "ascii",
      "box-drawing",
      "marketing"
    ],
    "instruction": "Build <ReachRuleTable tiers? rows? title? className?> as ONE CSS grid with gridTemplateColumns `minmax(12rem,1.4fr) repeat(N, minmax(6rem,1fr))`, over tiers: CompareTier[] ({id,name,price}) in ascending price order and rows: CompareFeatureRow[] ({id, label, note?, support: (boolean|string)[]}) with one support entry per tier (false = excluded, true = plainly included, a string = the tier-specific value such as \"30 days\", rendered verbatim under the rule). Each row's label cell is a real <button data-compare-row={id} aria-pressed> that PINS the row on click. THE MECHANIC: for each row compute reach = the highest index i where support[i] is truthy (−1 if none). An aria-hidden absolutely-positioned <span data-reach-rule> is laid over the row, left = the x-centre of tier column 0 and width = (x-centre of column `reach` − left), both taken from a single cached measurement pass that reads getBoundingClientRect on the N header cells plus one offscreen 40-glyph mono probe for the exact 1ch advance; the pass is re-run by a ResizeObserver on the grid root only when its width moves by more than 2px (WIDTH_EPSILON), never per frame. The rule's text is generated per cell at that 1ch pitch: each glyph slot is attributed to the nearest tier centre and drawn `─` when that tier includes the feature or `┄` when it does NOT — so a non-monotone matrix (a feature a mid tier lacks but a higher tier has) reads as a dotted gap inside an otherwise solid run instead of being smoothed away — and the final character is `┤`, held in its own child span so it alone can take accent ink. The rule sits on the row's first-line centre with `white-space:pre; letter-spacing:0; overflow:hidden`. RESTING INK: every row's rule is rendered at full reach at REST_OPACITY 0.42 in --foreground, so the idle frame is a descending staircase of faint rules each capped by ┤ under a different tier — the comparison is legible with no interaction. ACTIVATION: on pointerenter or focus of the row's button that rule steps to opacity 1 and RE-DRAWS from zero glyphs to full reach on one shared rAF (a single loop pumping a Map of in-flight rows, self-cancelling when the map empties) at 14ms per cell with the total capped at 320ms — a per-cell reveal rather than a CSS width transition, so the ┤ terminator lands on its exact cell instead of sliding through the intermediate ones — and every tier column that lacks the feature drops to opacity 0.35. On pointerleave/blur the rule retracts right-to-left at STEP_OUT_MS 9ms per cell and relaxes back to 0.42 unless the row is pinned. The measurement pass re-runs once on document.fonts.ready, because the 1ch probe measured before the mono face swaps in gives the wrong pitch and every ┤ would land off its column centre. Only one row is active at a time; a pinned row stays at full ink, keeps aria-pressed=true and is what the rail relaxes back to. HEADER TALLY: under each tier's name and price sits a mono completeness readout — a 12-cell `▪`/`·` track filled to round(12 * included[i] / rows.length) followed by the literal fraction (`9/12`), where included[i] is the number of rows that tier actually includes. RESPONSIVE: the same ResizeObserver flips the component below 720px to one stacked card per tier — a feature list with ● / ○ per row, the tier-specific value on the right, and the same 12-cell track at the card head; the glyph rules are decoration and are simply not rendered there. Colour is token-only — --border for rules and separators, --foreground for ink, --ns-muted for notes, prices and values, --ns-accent used ONLY on the focus-visible outline and on the ┤ terminator of the pinned row. Screen readers get the matrix as role=table/row/rowheader/cell with a per-cell sr-only \"included\" / \"not included\" / value string, so no meaning lives only in the glyphs. prefers-reduced-motion (watched live via matchMedia) drops the per-cell stagger entirely: the rule snaps to full reach and full ink in a single paint and relaxes the same way. Zero dependencies. Props: tiers (CompareTier[]), rows (CompareFeatureRow[]), title (accessible name for the matrix), className."
  },
  "type": "registry:ui"
}