{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "night-store",
  "title": "Night Store",
  "description": "A cache diagnostics rack where every key-group is a storage-heater brick holding heat: a hit charges it toward full glyph-density stipple, idle time cools it on a decay matched to its own TTL, and a real eviction contracts the rack by exactly one slot. Keys hit over and over flicker cold-hot instead of settling: the visual signature of a thrashing cache.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/night-store/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { CSSProperties } from \"react\";\n\n// ---------------------------------------------------------------------------\n// HeaterRack — a cache diagnostics panel where every key-group is a\n// storage-heater brick holding heat. One rule drives every brick: its\n// temperature T is a pure function of elapsed time since its last hit,\n// T(dt) = exp(-dt / tau) with tau = ttlMs/3000 seconds — chosen so a\n// never-hit entry already reads visibly cold at roughly a third of its\n// lifetime, early enough to PREDICT an eviction rather than report one. A\n// hit resets a brick's clock (T snaps to 1); the \"single fast charge ease\"\n// the brief asks for is realized as a plain ~220ms CSS opacity transition on\n// each stipple dot, not a hand-rolled JS ramp — the same trick this\n// registry already uses for its other scalar-driven surfaces (see\n// button-cooldown-heat's --heat). Temperature never touches color: it is\n// rendered purely as glyph-density STIPPLE (a fixed pool of deterministic,\n// per-brick dot positions, radially ranked so a warming brick fills from its\n// own center outward) quantized to a 5-level ramp — level = floor(T*5).\n//\n// Every visual property of a brick derives from ITS OWN T and nothing else.\n// There is no shared clock, stagger, or wave across the rack: two bricks\n// with identical hit histories always look identical, and a brick being hit\n// over and over while its neighbors sit idle visibly flickers cold-hot\n// against a static field — that contrast is the component's only\n// diagnostic value, so nothing here is allowed to move the whole rack in\n// unison.\n//\n// The rack's order is exactly the order of the `entries` prop and is NEVER\n// re-sorted by temperature — re-sorting would make it impossible to track\n// one brick's heat over time, which is the entire point of a diagnostics\n// view. An id that drops out of `entries` (a real eviction) doesn't vanish;\n// it plays a brief \"leaving\" contraction (max-width/opacity/margin easing\n// to zero) and only then leaves the DOM, so the rack visibly contracts by\n// exactly one slot instead of jump-cutting.\n//\n// Each brick is `role=\"listitem\"` carrying the full diagnostic sentence in\n// its own aria-label (key-group, hit count, a T/tau-derived eviction\n// forecast) so a screen reader can walk the list without touching anything.\n// The stipple SVG itself is aria-hidden. Anything beyond that summary — raw\n// temperature, TTL, last-hit age — sits behind a real per-brick disclosure\n// button (never a hover-only tooltip), which is also what keeps the\n// component keyboard-reachable: a diagnostics grid otherwise has no natural\n// \"control\" at all. Header stats (hit ratio, entry count, evictions/min)\n// are live numbers; a separate sr-only aria-live=polite region announces\n// only when `hitRatio` crosses the caller-supplied\n// `hitRatioAlertThreshold`, never on every tick.\n//\n// DOM + SVG + CSS only, no canvas. Every ink value is --foreground over\n// --ns-muted/--border/--background; --ns-accent appears solely on the\n// disclosure button's focus ring.\n// ---------------------------------------------------------------------------\n\nexport interface CacheEntryInput {\n  /** stable identity for this cache entry/shard */\n  id: string;\n  /** the key-group label rendered on the brick, e.g. \"user:profile\" */\n  keyGroup: string;\n  /** this entry's TTL in ms — tau (the decay time-constant) is ttlMs / 3 */\n  ttlMs: number;\n  /** cumulative hit count, owned by the caller */\n  hitCount: number;\n  /** epoch ms of this entry's most recent hit (or creation, if never hit again) */\n  lastHitAt: number;\n}\n\nexport interface HeaterRackProps {\n  /**\n   * The tracked key-groups, in the fixed diagnostic order the rack renders\n   * them — this order is never touched by the component. An id that drops\n   * out of a later `entries` array is treated as a real eviction and\n   * animates out; a new id is appended as a new brick.\n   */\n  entries: CacheEntryInput[];\n  /** rolling hit ratio, 0..1, shown in the header. Caller computes this — the rack doesn't infer it from `entries`. */\n  hitRatio: number;\n  /** evictions observed in the last 60s, shown in the header. */\n  evictionsPerMin: number;\n  /** if set, the sr-only live region announces only when hitRatio crosses this value (either direction). */\n  hitRatioAlertThreshold?: number;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\ninterface BrickRow {\n  id: string;\n  entry: CacheEntryInput;\n  leaving: boolean;\n}\n\nconst POOL_SIZE = 16;\nconst EVICT_TH = 0.15; // T below this reads as \"at risk now\" in the forecast text\nconst POLL_MS_NORMAL = 220;\nconst POLL_MS_REDUCED = 1000;\nconst LEAVE_MS = 260;\n\nfunction hashStr(s: string): number {\n  let h = 2166136261;\n  for (let i = 0; i < s.length; i++) {\n    h ^= s.charCodeAt(i);\n    h = Math.imul(h, 16777619);\n  }\n  return h >>> 0;\n}\n\nfunction mulberry32(seed: number) {\n  let a = seed;\n  return function () {\n    a = (a + 0x6d2b79f5) | 0;\n    let t = Math.imul(a ^ (a >>> 15), 1 | a);\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\n/** Deterministic, per-id stipple pool in normalized [0,1] space, ranked by\n *  distance from center so the reveal order (see `visibleCount`) fills a\n *  warming brick from its own core outward. Computed once per id and cached\n *  on a ref for the component's lifetime — positions never change. */\nfunction stipplePool(id: string): { x: number; y: number }[] {\n  const rng = mulberry32(hashStr(id));\n  const pts = Array.from({ length: POOL_SIZE }, () => ({ x: rng(), y: rng() }));\n  pts.sort((a, b) => Math.hypot(a.x - 0.5, a.y - 0.5) - Math.hypot(b.x - 0.5, b.y - 0.5));\n  return pts;\n}\n\nfunction computeT(nowMs: number, entry: CacheEntryInput): number {\n  const dt = Math.max(0, (nowMs - entry.lastHitAt) / 1000);\n  if (dt === 0) return 1;\n  const tau = Math.max(0.05, entry.ttlMs / 3000);\n  return Math.exp(-dt / tau);\n}\n\nfunction levelFromT(t: number): number {\n  return Math.min(4, Math.max(0, Math.floor(t * 5)));\n}\n\nfunction visibleCount(level: number): number {\n  return Math.round((level / 4) * POOL_SIZE);\n}\n\nfunction fmtDuration(ms: number): string {\n  const s = Math.max(0, Math.round(ms / 1000));\n  if (s < 60) return `${s}s`;\n  return `${Math.floor(s / 60)}m ${s % 60}s`;\n}\n\nfunction forecastText(t: number, entry: CacheEntryInput): string {\n  if (t >= 0.94) return \"just hit, full heat\";\n  if (t <= EVICT_TH) return \"cold, at risk of eviction now\";\n  const tau = Math.max(0.05, entry.ttlMs / 3000);\n  const secs = Math.max(1, Math.round(tau * Math.log(t / EVICT_TH)));\n  return `cooling, likely evicted in ~${secs}s`;\n}\n\nfunction useReducedMotion(): boolean {\n  const [reduced, setReduced] = useState(false);\n  useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    setReduced(mq.matches);\n    const onChange = () => setReduced(mq.matches);\n    mq.addEventListener(\"change\", onChange);\n    return () => mq.removeEventListener(\"change\", onChange);\n  }, []);\n  return reduced;\n}\n\nexport function HeaterRack({\n  entries,\n  hitRatio,\n  evictionsPerMin,\n  hitRatioAlertThreshold,\n  className = \"\",\n}: HeaterRackProps) {\n  const uid = useId();\n  const reduced = useReducedMotion();\n\n  // -- ordered rows: mirrors `entries` exactly while an id is present, keeps\n  // a departed id around (marked `leaving`) just long enough to animate the\n  // rack's contraction, and never reorders anything itself.\n  const [rows, setRows] = useState<BrickRow[]>(() =>\n    entries.map((entry) => ({ id: entry.id, entry, leaving: false }))\n  );\n  const leaveTimersRef = useRef<Map<string, number>>(new Map());\n\n  useEffect(() => {\n    const byId = new Map(entries.map((e) => [e.id, e]));\n    setRows((prev) => {\n      const next: BrickRow[] = [];\n      const seen = new Set<string>();\n      for (const row of prev) {\n        seen.add(row.id);\n        const live = byId.get(row.id);\n        if (live) {\n          next.push({ id: row.id, entry: live, leaving: false });\n        } else if (row.leaving) {\n          next.push(row); // already animating out\n        } else {\n          next.push({ ...row, leaving: true });\n          const timer = window.setTimeout(() => {\n            setRows((cur) => cur.filter((r) => r.id !== row.id));\n            leaveTimersRef.current.delete(row.id);\n          }, LEAVE_MS);\n          const prior = leaveTimersRef.current.get(row.id);\n          if (prior) window.clearTimeout(prior);\n          leaveTimersRef.current.set(row.id, timer);\n        }\n      }\n      for (const entry of entries) {\n        if (!seen.has(entry.id)) next.push({ id: entry.id, entry, leaving: false });\n      }\n      return next;\n    });\n  }, [entries]);\n\n  useEffect(() => {\n    const timers = leaveTimersRef.current;\n    return () => {\n      timers.forEach((t) => window.clearTimeout(t));\n      timers.clear();\n    };\n  }, []);\n\n  // -- periodic recompute of T for every brick. T is a pure function of\n  // elapsed time, so there's nothing to simulate frame-by-frame — this just\n  // re-samples it, at 1Hz under reduced motion per the spec, faster\n  // otherwise so the fast charge-ease reads smoothly.\n  const [tick, setTick] = useState(0);\n  const reducedRef = useRef(reduced);\n  reducedRef.current = reduced;\n  useEffect(() => {\n    let id = 0;\n    const schedule = () => {\n      id = window.setTimeout(() => {\n        setTick((t) => t + 1);\n        schedule();\n      }, reducedRef.current ? POLL_MS_REDUCED : POLL_MS_NORMAL);\n    };\n    schedule();\n    return () => window.clearTimeout(id);\n  }, []);\n\n  const now = useMemo(() => Date.now(), [tick]);\n\n  const poolCacheRef = useRef<Map<string, { x: number; y: number }[]>>(new Map());\n  const poolFor = (id: string) => {\n    let pool = poolCacheRef.current.get(id);\n    if (!pool) {\n      pool = stipplePool(id);\n      poolCacheRef.current.set(id, pool);\n    }\n    return pool;\n  };\n\n  const [expanded, setExpanded] = useState<Set<string>>(() => new Set());\n  const toggleExpanded = (id: string) => {\n    setExpanded((prev) => {\n      const next = new Set(prev);\n      if (next.has(id)) next.delete(id);\n      else next.add(id);\n      return next;\n    });\n  };\n\n  // -- threshold-crossing announcement only, never a per-tick readout.\n  const [announce, setAnnounce] = useState(\"\");\n  const prevAboveRef = useRef<boolean | null>(null);\n  useEffect(() => {\n    if (hitRatioAlertThreshold == null) return;\n    const above = hitRatio >= hitRatioAlertThreshold;\n    if (prevAboveRef.current !== null && prevAboveRef.current !== above) {\n      const pct = Math.round(hitRatioAlertThreshold * 100);\n      setAnnounce(\n        above\n          ? `Hit ratio recovered above ${pct}%.`\n          : `Hit ratio dropped below ${pct}% — cache under pressure.`\n      );\n    }\n    prevAboveRef.current = above;\n  }, [hitRatio, hitRatioAlertThreshold]);\n\n  return (\n    <div className={`ns-hr-root ${className}`}>\n      <style>{CSS}</style>\n\n      <dl className=\"m-0 flex flex-wrap gap-x-6 gap-y-2 p-0\" aria-label=\"Cache health\">\n        <div className=\"flex flex-col gap-0.5\">\n          <dt className=\"m-0 font-mono text-[10px] uppercase tracking-wider text-ns-muted\">Hit ratio</dt>\n          <dd className=\"m-0 font-mono text-sm font-semibold tabular-nums text-foreground\">\n            {Math.round(hitRatio * 100)}%\n          </dd>\n        </div>\n        <div className=\"flex flex-col gap-0.5\">\n          <dt className=\"m-0 font-mono text-[10px] uppercase tracking-wider text-ns-muted\">Entries</dt>\n          <dd className=\"m-0 font-mono text-sm font-semibold tabular-nums text-foreground\">{entries.length}</dd>\n        </div>\n        <div className=\"flex flex-col gap-0.5\">\n          <dt className=\"m-0 font-mono text-[10px] uppercase tracking-wider text-ns-muted\">Evictions/min</dt>\n          <dd className=\"m-0 font-mono text-sm font-semibold tabular-nums text-foreground\">{evictionsPerMin}</dd>\n        </div>\n      </dl>\n\n      <ul className=\"ns-hr-rack m-0 mt-3 flex flex-wrap p-0\" style={{ listStyle: \"none\" }}>\n        {rows.map((row, index) => {\n          const t = row.leaving ? 0 : computeT(now, row.entry);\n          const level = levelFromT(t);\n          const shown = visibleCount(level);\n          const pool = poolFor(row.id);\n          const isExpanded = expanded.has(row.id);\n          const detailId = `${uid}-detail-${row.id}`;\n          const forecast = forecastText(t, row.entry);\n          const hitNoun = row.entry.hitCount === 1 ? \"hit\" : \"hits\";\n          const ariaLabel = `${row.entry.keyGroup}, ${row.entry.hitCount} ${hitNoun}, ${forecast}`;\n\n          return (\n            <li\n              key={row.id}\n              role=\"listitem\"\n              aria-label={ariaLabel}\n              data-ns-brick-index={index}\n              className={`ns-hr-brick ${row.leaving ? \"ns-hr-brick-leaving\" : \"\"}`}\n              style={{ \"--t\": t.toFixed(3) } as CSSProperties}\n            >\n              <div className=\"ns-hr-stipple\" aria-hidden=\"true\">\n                <svg viewBox=\"0 0 1 1\" preserveAspectRatio=\"none\" focusable=\"false\">\n                  {pool.map((p, k) => (\n                    <circle\n                      key={k}\n                      cx={p.x}\n                      cy={p.y}\n                      r={0.055}\n                      className=\"ns-hr-dot\"\n                      style={{ opacity: k < shown ? 1 : 0 }}\n                    />\n                  ))}\n                </svg>\n              </div>\n\n              <div className=\"mt-1.5 flex items-baseline justify-between gap-1.5\">\n                <span className=\"truncate font-mono text-[10px] text-foreground\">{row.entry.keyGroup}</span>\n                <span className=\"shrink-0 font-mono text-[10px] tabular-nums text-ns-muted\">\n                  {row.entry.hitCount}\n                </span>\n              </div>\n\n              <button\n                type=\"button\"\n                data-ns-disclosure\n                aria-expanded={isExpanded}\n                aria-controls={detailId}\n                onClick={() => toggleExpanded(row.id)}\n                className=\"ns-hr-disclosure mt-1.5 flex w-full items-center justify-center rounded-[6px] border border-border py-0.5 text-[11px] leading-none text-ns-muted transition-colors duration-150 hover:border-foreground/30 hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent\"\n              >\n                <span className=\"sr-only\">\n                  {isExpanded ? \"Hide\" : \"Show\"} details for {row.entry.keyGroup}\n                </span>\n                <span aria-hidden=\"true\">{isExpanded ? \"−\" : \"+\"}</span>\n              </button>\n\n              {isExpanded ? (\n                <dl\n                  id={detailId}\n                  data-ns-detail\n                  className=\"mt-1.5 flex flex-col gap-0.5 border-t border-border pt-1.5\"\n                >\n                  <div className=\"flex justify-between gap-2\">\n                    <dt className=\"m-0 font-mono text-[9px] text-ns-muted\">Temp</dt>\n                    <dd className=\"m-0 font-mono text-[9px] tabular-nums text-foreground\">{Math.round(t * 100)}%</dd>\n                  </div>\n                  <div className=\"flex justify-between gap-2\">\n                    <dt className=\"m-0 font-mono text-[9px] text-ns-muted\">TTL</dt>\n                    <dd className=\"m-0 font-mono text-[9px] tabular-nums text-foreground\">\n                      {fmtDuration(row.entry.ttlMs)}\n                    </dd>\n                  </div>\n                  <div className=\"flex justify-between gap-2\">\n                    <dt className=\"m-0 font-mono text-[9px] text-ns-muted\">Last hit</dt>\n                    <dd className=\"m-0 font-mono text-[9px] tabular-nums text-foreground\">\n                      {fmtDuration(now - row.entry.lastHitAt)} ago\n                    </dd>\n                  </div>\n                  <div className=\"flex justify-between gap-2\">\n                    <dt className=\"m-0 font-mono text-[9px] text-ns-muted\">Forecast</dt>\n                    <dd className=\"m-0 text-right font-mono text-[9px] text-foreground\">{forecast}</dd>\n                  </div>\n                </dl>\n              ) : null}\n            </li>\n          );\n        })}\n      </ul>\n\n      <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {announce}\n      </span>\n    </div>\n  );\n}\n\nconst CSS = `\n.ns-hr-brick {\n  position: relative;\n  box-sizing: border-box;\n  width: 104px;\n  max-width: 104px;\n  margin: 0 8px 8px 0;\n  padding: 8px;\n  border: 1px solid var(--border);\n  border-radius: 6px;\n  background-color: color-mix(in srgb, var(--background), var(--ns-muted) calc(var(--t, 0) * 7%));\n  overflow: hidden;\n  transition: max-width 260ms cubic-bezier(0.4,0,0.2,1), opacity 260ms ease-in, margin-right 260ms ease-in, padding-left 260ms ease-in, padding-right 260ms ease-in, border-width 260ms ease-in, background-color 220ms ease-out;\n}\n.ns-hr-brick-leaving {\n  max-width: 0;\n  width: 0;\n  opacity: 0;\n  margin-right: 0;\n  padding-left: 0;\n  padding-right: 0;\n  border-left-width: 0;\n  border-right-width: 0;\n}\n.ns-hr-stipple { width: 100%; height: 40px; }\n.ns-hr-stipple svg { width: 100%; height: 100%; display: block; }\n.ns-hr-dot { fill: var(--foreground); transition: opacity 220ms ease-out; }\n@media (prefers-reduced-motion: reduce) {\n  .ns-hr-brick { transition: none; }\n  .ns-hr-dot { transition: none; }\n}\n`;\n",
      "type": "registry:ui",
      "target": "components/ui/night-store.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "cache",
      "dashboard",
      "monitoring",
      "stipple",
      "svg",
      "data-visualization",
      "disclosure",
      "accessibility"
    ],
    "instruction": "Build a cache diagnostics panel (`HeaterRack`) where every tracked key-group renders as a storage-heater brick holding heat. Props: `entries: CacheEntryInput[]` (each `{ id, keyGroup, ttlMs, hitCount, lastHitAt }`, owned and updated by the caller from real cache events), `hitRatio: number` (0..1, caller-computed rolling ratio), `evictionsPerMin: number`, an optional `hitRatioAlertThreshold?: number`, and `className`. MECHANISM (the one rule that drives every brick, and the falsifiable constraint of the whole component): a brick's temperature T is a PURE FUNCTION of elapsed time since its own last hit — `T(dt) = exp(-dt / tau)` where `tau = ttlMs / 3000` seconds (chosen so a never-hit entry already reads visibly cold at roughly a third of its lifetime, early enough to PREDICT an eviction rather than report one after the fact) and `dt = (now - lastHitAt) / 1000`; a hit simply resets `lastHitAt` to now, which snaps T back to 1. Every visual property of a brick derives from that brick's OWN T and nothing else — no global shimmer, wave, or stagger is allowed anywhere in the rack, because once bricks move together for ambience the thrash-flicker of a key being hit-then-cooled-then-hit-again over and over becomes invisible, and that flicker is the component's only diagnostic value. Re-sample T periodically (a plain `setTimeout` re-tick loop, not a physics simulation — there is nothing to accumulate since T is a pure function of elapsed time) at roughly 220ms under normal motion and exactly 1000ms (1Hz) under `prefers-reduced-motion: reduce`, per `matchMedia` with a live change listener. RENDERING — glyph-density stipple, never color: temperature is communicated purely by how many of a brick's dots are visible, quantized to a fixed 5-level ramp (`level = floor(T*5)`, clamped 0-4). Each brick has its own small fixed pool of 16 dot positions in a `viewBox=\"0 0 1 1\"` SVG, generated once by a deterministic per-id PRNG (an FNV-1a-style hash of the entry `id` feeding a mulberry32 generator, same technique as this registry's other stipple components) and then sorted by distance from the brick's own center — so `visibleCount(level) = round((level/4)*16)` dots reveal outward from that brick's core as it warms, giving even a single brick a 'hot core, cool fringe' read, and the same pattern makes the WHOLE RACK read as a hot-core/cool-fringe texture once several bricks are hot at once. Every dot is `fill: var(--foreground)`; a brick's own background is a subtle `color-mix(in srgb, var(--background), var(--ns-muted) calc(var(--t)*7%))` wash (still grayscale, not a color ramp) driven by a `--t` custom property written straight onto the `<li>`. A brick that JUST got hit (T snapping toward 1, more dots turning on) or is cooling (dots turning back off) gets the SAME plain CSS `transition: opacity 220ms ease-out` on each dot — this is the entire 'single fast charge ease' the mechanism calls for: no hand-written JS ramp, just a short opacity transition doing the easing for both directions, stripped to an instant step under `prefers-reduced-motion: reduce`. LAYOUT AND ORDER: bricks render in a plain flex-wrap `<ul>`, in EXACTLY the order of the `entries` prop — the rack is never re-sorted by temperature at any point, because re-sorting would make it impossible to watch one brick's heat over time, which is the entire point of the view. When an id disappears from a later `entries` array (a real eviction happened upstream), the component doesn't unmount it immediately: it keeps the row, marks it `leaving`, and animates `max-width`/`opacity`/`margin`/`padding`/`border-width` to zero over 260ms (a small, real contraction of the rack by exactly one slot) before finally removing it from state — implemented by keeping an internal `rows` array reconciled against `entries` in a `useEffect`, with per-id `setTimeout` cleanup on unmount. ACCESSIBILITY: each brick is `<li role=\"listitem\">` with an `aria-label` stating the full diagnostic sentence as text — `\"{keyGroup}, {hitCount} hits, {forecast}\"` — where `forecast` is derived purely from T and tau: `\"just hit, full heat\"` when T >= 0.94, `\"cold, at risk of eviction now\"` when T <= 0.15, otherwise `\"cooling, likely evicted in ~{round(tau * ln(T/0.15))}s\"` — a genuine prediction computed from the same decay constant driving the visual, not a canned string. The stipple SVG itself is `aria-hidden`. Beyond that label, every extra numeric fact (raw temperature %, TTL, time since last hit, the same forecast sentence) sits behind a real per-brick disclosure `<button data-ns-disclosure aria-expanded aria-controls>` (a '+' / '−' glyph with an sr-only accessible name, 'Show/Hide details for {keyGroup}') that reveals a `<dl data-ns-detail>` of those facts on click — never a hover-only tooltip, and this disclosure button is also what keeps the component genuinely keyboard-reachable, since a passive diagnostics grid otherwise exposes no natural control. The header renders three live stats as a `<dl>` — hit ratio, entry count, evictions/min — as plain visible text that simply re-renders on every prop change (not itself wrapped in `aria-live`, to avoid spamming assistive tech on every tick); a separate `sr-only` `role=\"status\" aria-live=\"polite\"` span announces a sentence ONLY when `hitRatio` crosses the caller-supplied `hitRatioAlertThreshold` in either direction ('Hit ratio dropped below 70% — cache under pressure.' / 'Hit ratio recovered above 70%.'), tracked via a ref holding the previous above/below state so re-renders at the same ratio never re-announce. TOKENS: --foreground for every dot and text value, --border for brick borders and the disclosure button's resting border, --ns-muted for secondary text and the background wash, --ns-accent ONLY on the disclosure button's `focus-visible` outline — no hex, no `rgb()`/`hsl()`, no Tailwind palette classes, no canvas. DEMO: a 12-key-group cache (`user:session`, `product:price`, `product:stock`, etc.) with short 6-45s TTLs so decay and eviction play out on screen, driven by a weighted random-hit interval (two key-groups deliberately over-weighted so they visibly thrash), a rare real eviction of whichever entry is coldest once nothing has been evicted in the last ~22s, and rolling hit-ratio/evictions-per-minute stats recomputed from a trailing event window — entirely the demo's own simulation, so the rack animates ambiently without needing any synthetic autoplay driver input."
  },
  "type": "registry:ui"
}