{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "quoin-lockup",
  "title": "Quoin Lockup",
  "description": "A bento grid whose only variable is pressure, not cell assignment: four quoins hold a fixed five-tile arrangement flush, pressure creeps down until a tile pies out of plane, and a quoin-by-quoin re-key — occasionally a full planer sweep — brings it back, forever.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/quoin-lockup/component.tsx",
      "content": "\"use client\";\n\nimport { useLayoutEffect, useMemo, useRef, useState, type KeyboardEvent } from \"react\";\n\n// ---------------------------------------------------------------------------\n// QuoinLockup — a bento grid whose interesting variable is PRESSURE, not cell\n// assignment. Five tiles sit in a FIXED arrangement inside a steel chase; the\n// gutters between them are real furniture with discrete quantised widths, and\n// four quoins (opposing wedge pairs at the right edge and the foot) supply the\n// pressure that holds the whole lockup flush. Pressure creeps down forever;\n// once a tile's seeded hold value crosses the lift threshold it pies — sinks\n// out of plane, tilts, grows a contact shadow — and the operator re-keys the\n// quoins one at a time to bring it back. Nothing ever changes which cell a\n// tile occupies: that is the entire difference from grid-bento-dense (which\n// re-packs on `grid-auto-flow: dense`) and grid-bento-ascii (which re-spans a\n// cell across every seam track). Here the arrangement is a constant; only the\n// pressure — and, downstream of it, gutter width and tile plane — moves.\n//\n// D8 (round 13 decisions): this sits near letterpress the same way\n// convert-foil-block does, but the two are disjoint. foil-block's variable is\n// a dual TRANSFER THRESHOLD (heat + force) deciding whether a mark takes on\n// paper at all; it renders no bed of type. This component's variable is\n// PRESSURE distributing an already-fixed layout — it never decides whether\n// content \"takes\", only how tightly the grid holding it is locked up.\n//\n// Colour is pure CSS: every value comes from --background/--foreground/\n// --ns-muted/--border/--ns-accent through Tailwind utilities or an inline\n// `color-mix(in srgb, var(--x), var(--y) N%)`, with light/dark deltas carried\n// by a `dark:` class flipping a small custom property the color-mix reads.\n// Nothing here is a raster surface, so there is no getComputedStyle/\n// MutationObserver token pipeline to race against first paint — the browser\n// resolves every var() and color-mix() synchronously at paint time. This\n// mirrors grid-bento-dense, the nearest sibling in this collection (\"All\n// color from tokens via Tailwind utility classes, zero canvas\").\n// ---------------------------------------------------------------------------\n\nexport interface QuoinLockupTile {\n  id: string;\n  title: string;\n  body?: string;\n  href?: string;\n}\n\nexport interface QuoinLockupProps {\n  /** Exactly 5 tiles, fixed arrangement (2x2 hero, two elongated, two 1x1). */\n  tiles?: QuoinLockupTile[];\n  /** How often (in press cycles) the planer sweeps and flushes the whole form. */\n  planerEvery?: number;\n  className?: string;\n}\n\nconst DEFAULT_TILES: QuoinLockupTile[] = [\n  {\n    id: \"hero\",\n    title: \"One workspace, everything in it\",\n    body: \"Docs, tasks and reviews read from the same record, so nothing drifts out of sync between them.\",\n    href: \"#\",\n  },\n  { id: \"tall\", title: \"Fast on a cold start\", body: \"Pages stream in as you open them, with no build step to wait on.\", href: \"#\" },\n  { id: \"wide\", title: \"Version history\", body: \"Roll any document back to an earlier state without leaving the page.\", href: \"#\" },\n  { id: \"a\", title: \"Scoped access\", body: \"Invite a team and choose exactly what each role can see.\", href: \"#\" },\n  { id: \"b\", title: \"Works offline\", body: \"Edits queue on the device and merge cleanly the moment you reconnect.\", href: \"#\" },\n];\n\n// -- real numbers (spec section 5) ------------------------------------------\nconst CHASE_INSET_F = 0.055;\nconst RAIL_STROKE_F = 0.012;\nconst U_F = 0.0125; // one pica, as a fraction of M\nconst LEGAL_LEVELS = [2, 3, 4, 6, 8]; // legal gutter widths, in u\nconst QUOIN_TRAVEL_U = 1.0;\nconst P_MAX = 0.93;\nconst P_MIN = 0.62;\nconst LIFT_THRESHOLD = 0.62;\nconst DECAY_RATE = 0.055; // P units / second\nconst DECAY_DURATION = (P_MAX - P_MIN) / DECAY_RATE; // ~5.64s, spec's \"5.6s\"\nconst QUOIN_COUNT = 4;\nconst QUOIN_INTERVAL = 0.64; // s between quoins starting their turn\nconst QUOIN_ROTATE = 0.32; // s for a key to turn 90deg\nconst QUOIN_STEP_UP = 0.19; // P per quoin, clamped at P_MAX\nconst REKEY_DURATION = QUOIN_COUNT * QUOIN_INTERVAL; // 2.56s, spec's \"~2.6s\"\nconst CYCLE = DECAY_DURATION + REKEY_DURATION; // ~8.2s, spec's period\nconst PLANER_DURATION = 0.7;\nconst SINK_DEPTH_F = 0.028; // fraction of M\nconst SINK_SCALE = 0.022; // 1 - this at full sink -> 0.978\nconst SINK_TILT_DEG = 0.9;\nconst STATIC_TIME = 6.3;\n\n// fixed seed, never Math.random: same tile always loosens first\nconst JITTER = [-0.62, 0.35, -0.88, 0.71, 0.08];\nconst TILT_SIGN = [1, -1, 1, -1, 1];\n\ntype GutterKey = \"gx1\" | \"gx2\" | \"gy1\" | \"gy2\" | \"gy3\";\nconst FULL_GUTTERS: GutterKey[] = [\"gx1\", \"gx2\", \"gy1\", \"gy2\", \"gy3\"];\nconst COMPACT_GUTTERS: GutterKey[] = [\"gx1\", \"gy1\"];\n\n// which gutters border each tile, so hover/focus can step just those down a\n// legal size — nothing about the grid ever re-assigns, only these widths\nconst FULL_TILE_GUTTERS: GutterKey[][] = [\n  [\"gx2\", \"gy2\"], // hero: 2x2, cols 1-2 rows 1-2\n  [\"gx2\", \"gy2\"], // tall: 1x2, col 3 rows 1-2\n  [\"gy2\", \"gy3\"], // wide: 2x1, cols 1-2 row 3\n  [\"gx2\", \"gy2\", \"gy3\"], // a: 1x1, col 3 row 3\n  [\"gy3\"], // b: full-width foot course, row 4\n];\nconst COMPACT_TILE_GUTTERS: GutterKey[][] = [\n  [\"gy1\"], // wide: 2x1, cols 1-2 row 1\n  [\"gx1\", \"gy1\"], // a: 1x1, col 1 row 2\n  [\"gx1\", \"gy1\"], // b: 1x1, col 2 row 2\n];\n\nfunction quantize(target: number): number {\n  let best = LEGAL_LEVELS[0];\n  let bestDist = Infinity;\n  for (const level of LEGAL_LEVELS) {\n    const d = Math.abs(level - target);\n    if (d < bestDist) {\n      bestDist = d;\n      best = level;\n    }\n  }\n  return best;\n}\n\n// legal gutter width, in u, for a given pressure: tighter lockup -> narrower\n// furniture. Quantised, so this never reports a continuous value.\nfunction gutterLevelFor(p: number): number {\n  const clamped = Math.min(1, Math.max(0, p));\n  const target = 2 + (1 - clamped) * (8 - 2);\n  return quantize(target);\n}\n\nfunction smoothstep(x: number): number {\n  const t = Math.min(1, Math.max(0, x));\n  return t * t * (3 - 2 * t);\n}\n\ninterface QuoinLockupState {\n  p: number;\n  cyclePos: number;\n  rekeyActive: boolean;\n  activeQuoin: number; // -1 when none mid-turn\n  quoinTravel: number[]; // 0..1, one per quoin, uniform\n  keyDock: number; // which quoin the key sits at when idle (last one turned)\n  keyRotate: number; // 0..1 (0..90deg) for the quoin mid-turn\n  planerActive: boolean;\n  planerX: number; // 0..1 across the form\n}\n\n// Pure function of absolute time: no integration, so the same t always\n// produces the same frame. This is what makes STATIC_TIME byte-stable.\nfunction stateAt(t: number, planerEvery: number): QuoinLockupState {\n  const cycleIndex = Math.floor(t / CYCLE);\n  const cyclePos = t - cycleIndex * CYCLE;\n  const isPlanerCycle = planerEvery > 0 && (cycleIndex + 1) % planerEvery === 0;\n\n  let p: number;\n  let rekeyActive = false;\n  let activeQuoin = -1;\n  const quoinTravel = [0, 0, 0, 0];\n  let keyDock = QUOIN_COUNT - 1;\n  let keyRotate = 0;\n  let planerActive = false;\n  let planerX = 0;\n\n  if (cyclePos < DECAY_DURATION) {\n    p = P_MAX - DECAY_RATE * cyclePos;\n    // wedges ease back toward their resting travel as pressure relaxes\n    const travel = smoothstep((p - P_MIN) / (P_MAX - P_MIN));\n    quoinTravel[0] = quoinTravel[1] = quoinTravel[2] = quoinTravel[3] = travel;\n    keyDock = -1; // key is out, riding the last quoin from the previous cycle\n  } else {\n    rekeyActive = true;\n    const rk = cyclePos - DECAY_DURATION;\n    p = LIFT_THRESHOLD;\n    for (let i = 0; i < QUOIN_COUNT; i++) {\n      const start = i * QUOIN_INTERVAL;\n      if (rk < start) {\n        quoinTravel[i] = 0;\n        continue;\n      }\n      const local = rk - start;\n      const turnProgress = smoothstep(local / QUOIN_ROTATE);\n      quoinTravel[i] = turnProgress;\n      if (local < QUOIN_ROTATE) {\n        activeQuoin = i;\n        keyRotate = turnProgress;\n      } else {\n        keyDock = i;\n      }\n      // the +0.19 step begins 140ms into the turn and completes over the\n      // remaining 180ms (critically damped ease, approximated by smoothstep);\n      // each later quoin's iteration overwrites p with its own cumulative\n      // total, so the last quoin whose turn has started wins, which is\n      // exactly the running pressure after i completed steps plus this one\n      const stepProgress = smoothstep((local - 0.14) / 0.18);\n      p = Math.min(P_MAX, LIFT_THRESHOLD + i * QUOIN_STEP_UP + QUOIN_STEP_UP * stepProgress);\n    }\n    if (isPlanerCycle && rk < PLANER_DURATION) {\n      planerActive = true;\n      planerX = smoothstep(rk / PLANER_DURATION);\n    }\n  }\n\n  return { p, cyclePos, rekeyActive, activeQuoin, quoinTravel, keyDock, keyRotate, planerActive, planerX };\n}\n\nfunction sinkFracFor(p: number, index: number): number {\n  const h = p + 0.1 * JITTER[index];\n  if (h >= LIFT_THRESHOLD) return 0;\n  return Math.min(1, (LIFT_THRESHOLD - h) / LIFT_THRESHOLD);\n}\n\n// tile x-position, as a fraction of the chase width, used only to decide when\n// the planer bar has swept past a given tile (left column vs right column)\nconst FULL_TILE_X: number[] = [0.28, 0.83, 0.28, 0.83, 0.5];\nconst COMPACT_TILE_X: number[] = [0.5, 0.25, 0.75];\n\nexport function QuoinLockup({ tiles = DEFAULT_TILES, planerEvery = 3, className = \"\" }: QuoinLockupProps) {\n  const hostRef = useRef<HTMLDivElement | null>(null);\n  const [m, setM] = useState(0);\n  const [compact, setCompact] = useState(false);\n  const [hoveredId, setHoveredId] = useState<string | null>(null);\n  const [activeIdx, setActiveIdx] = useState(0);\n  const [reducedMotion, setReducedMotion] = useState(false);\n\n  const gridRef = useRef<HTMLDivElement | null>(null);\n  const tileRefs = useRef(new Map<string, HTMLAnchorElement | HTMLButtonElement>());\n  const quoinRefs = useRef<(HTMLDivElement | null)[]>([null, null, null, null]);\n  const keyRef = useRef<HTMLDivElement | null>(null);\n  const planerRef = useRef<HTMLDivElement | null>(null);\n  const railRef = useRef<HTMLDivElement | null>(null);\n\n  const rows = compact ? tiles.slice(0, 3) : tiles.slice(0, 5);\n  const gutterKeys = compact ? COMPACT_GUTTERS : FULL_GUTTERS;\n  const tileGutters = compact ? COMPACT_TILE_GUTTERS : FULL_TILE_GUTTERS;\n  const tileX = compact ? COMPACT_TILE_X : FULL_TILE_X;\n\n  // measure before paint so the form is never shown at the wrong size\n  useLayoutEffect(() => {\n    const host = hostRef.current;\n    if (!host) return;\n    const measure = (w: number) => {\n      setM(w);\n      setCompact(w < 320);\n    };\n    const rect = host.getBoundingClientRect();\n    measure(Math.min(rect.width, rect.height) || rect.width);\n    const ro = new ResizeObserver((entries) => {\n      const entry = entries[0];\n      if (!entry) return;\n      const box = entry.contentRect;\n      measure(Math.min(box.width, box.height) || box.width);\n    });\n    ro.observe(host);\n    return () => ro.disconnect();\n  }, []);\n\n  useLayoutEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    setReducedMotion(mq.matches);\n    const onChange = () => setReducedMotion(mq.matches);\n    mq.addEventListener(\"change\", onChange);\n    return () => mq.removeEventListener(\"change\", onChange);\n  }, []);\n\n  const u = m * U_F;\n\n  const applyFrame = useMemo(\n    () => (t: number) => {\n      const st = stateAt(t, planerEvery);\n\n      // gutters: pressure-derived legal width, minus one legal step for\n      // whichever gutters border the currently hovered/focused tile\n      const hoveredIndex = hoveredId ? rows.findIndex((c) => c.id === hoveredId) : -1;\n      const hoveredKeys = hoveredIndex >= 0 ? tileGutters[hoveredIndex] : [];\n      const baseLevel = gutterLevelFor(st.p);\n      const grid = gridRef.current;\n      if (grid) {\n        for (const key of gutterKeys) {\n          let level = baseLevel;\n          if (hoveredKeys.includes(key)) {\n            const idx = LEGAL_LEVELS.indexOf(level);\n            level = LEGAL_LEVELS[Math.max(0, idx - 1)];\n          }\n          // the track size lives on the grid container's own custom\n          // property — a property set on a gutter cell would not be\n          // inherited upward by its ancestor's grid-template\n          grid.style.setProperty(`--gw-${key}`, `${level * u}px`);\n        }\n      }\n\n      // tiles: sink, tilt, contact shadow, planer override\n      rows.forEach((tile, i) => {\n        const el = tileRefs.current.get(tile.id);\n        if (!el) return;\n        let sink = sinkFracFor(st.p, i);\n        if (st.planerActive && st.planerX >= tileX[i]) sink = 0;\n        const sinkPx = sink * SINK_DEPTH_F * m;\n        const scale = 1 - SINK_SCALE * sink;\n        const tilt = SINK_TILT_DEG * sink * TILT_SIGN[i];\n        el.style.setProperty(\"--sink\", `${sinkPx}px`);\n        el.style.setProperty(\"--tile-scale\", `${scale}`);\n        el.style.setProperty(\"--tile-tilt\", `${tilt}deg`);\n        el.style.setProperty(\"--shadow-o\", `${sink}`);\n        el.style.setProperty(\"--edge-o\", `${1 - sink}`);\n      });\n\n      // quoins + key\n      for (let i = 0; i < QUOIN_COUNT; i++) {\n        const el = quoinRefs.current[i];\n        if (el) el.style.setProperty(\"--travel\", `${st.quoinTravel[i] * QUOIN_TRAVEL_U * u}px`);\n      }\n      if (keyRef.current) {\n        const dockIndex = st.activeQuoin >= 0 ? st.activeQuoin : Math.max(0, st.keyDock);\n        keyRef.current.style.setProperty(\"--key-slot\", `${dockIndex}`);\n        keyRef.current.style.setProperty(\"--key-rotate\", `${st.keyRotate * 90}deg`);\n        keyRef.current.style.opacity = st.rekeyActive ? \"1\" : \"0\";\n      }\n      if (planerRef.current) {\n        planerRef.current.style.opacity = st.planerActive ? \"1\" : \"0\";\n        planerRef.current.style.setProperty(\"--planer-x\", `${st.planerX * 100}%`);\n      }\n    },\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [m, u, compact, hoveredId, planerEvery]\n  );\n\n  useLayoutEffect(() => {\n    if (m <= 0) return;\n\n    if (reducedMotion) {\n      applyFrame(STATIC_TIME);\n      return;\n    }\n\n    let raf = 0;\n    let running = false;\n    const origin = performance.now();\n\n    const loop = (now: number) => {\n      applyFrame((now - origin) / 1000);\n      raf = requestAnimationFrame(loop);\n    };\n    const wake = () => {\n      if (running) return;\n      running = true;\n      raf = requestAnimationFrame(loop);\n    };\n    const sleep = () => {\n      cancelAnimationFrame(raf);\n      running = false;\n    };\n\n    const host = hostRef.current;\n    let onScreen = true;\n    const io = host\n      ? new IntersectionObserver(\n          (entries) => {\n            onScreen = entries.some((en) => en.isIntersecting);\n            if (!onScreen) sleep();\n            else if (!document.hidden) wake();\n          },\n          { threshold: 0 }\n        )\n      : null;\n    if (host && io) io.observe(host);\n\n    const onVis = () => {\n      if (document.hidden) sleep();\n      else if (onScreen) wake();\n    };\n    document.addEventListener(\"visibilitychange\", onVis);\n    wake();\n\n    return () => {\n      sleep();\n      io?.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVis);\n    };\n  }, [m, reducedMotion, applyFrame]);\n\n  const onKeyDown = (e: KeyboardEvent<HTMLElement>, index: number) => {\n    const count = rows.length;\n    let next = index;\n    switch (e.key) {\n      case \"ArrowRight\":\n      case \"ArrowDown\":\n        e.preventDefault();\n        next = (index + 1) % count;\n        break;\n      case \"ArrowLeft\":\n      case \"ArrowUp\":\n        e.preventDefault();\n        next = (index - 1 + count) % count;\n        break;\n      default:\n        return;\n    }\n    setActiveIdx(next);\n    const el = tileRefs.current.get(rows[next].id);\n    el?.focus();\n  };\n\n  // grid-column/row spans are module-level constants, never written after\n  // mount — pressure changes gutter TRACK SIZE (the --gw-* custom\n  // properties, set imperatively on the grid container in applyFrame), spans\n  // never change.\n  const fullTemplateCols = \"minmax(0,1fr) var(--gw-gx1,3px) minmax(0,1fr) var(--gw-gx2,3px) minmax(0,1fr)\";\n  const fullTemplateRows =\n    \"minmax(0,1fr) var(--gw-gy1,3px) minmax(0,1fr) var(--gw-gy2,3px) minmax(0,1fr) var(--gw-gy3,3px) minmax(0,1fr)\";\n  const compactTemplateCols = \"minmax(0,1fr) var(--gw-gx1,3px) minmax(0,1fr)\";\n  const compactTemplateRows = \"minmax(0,1fr) var(--gw-gy1,3px) minmax(0,1fr)\";\n\n  return (\n    <div ref={hostRef} className={`relative h-full w-full min-h-[240px] ${className}`}>\n      {/* frame layer: chase rail + quoins + key + planer. Positioned as\n          fractions of the chase, so it needs nothing from the tile grid. */}\n      <div aria-hidden className=\"pointer-events-none absolute inset-0 z-0\">\n        <div\n          ref={railRef}\n          className=\"absolute [--rail-a:72%] dark:[--rail-a:100%]\"\n          style={{\n            inset: m * CHASE_INSET_F,\n            borderWidth: Math.max(1, m * RAIL_STROKE_F),\n            borderStyle: \"solid\",\n            borderColor: \"color-mix(in srgb, var(--foreground) var(--rail-a), transparent)\",\n            borderRadius: 2,\n          }}\n        />\n        {[0, 1, 2, 3].map((i) => {\n          const edge = i < 2 ? \"right\" : \"foot\";\n          const along = edge === \"right\" ? (i === 0 ? 0.32 : 0.68) : i === 2 ? 0.32 : 0.68;\n          const len = u * 3;\n          const thick = u * 1.4;\n          return (\n            <div\n              key={i}\n              ref={(el) => {\n                quoinRefs.current[i] = el;\n              }}\n              className=\"absolute [--travel:0px]\"\n              style={\n                edge === \"right\"\n                  ? {\n                      right: m * CHASE_INSET_F - thick * 0.4,\n                      top: `calc(${along * 100}% - ${len / 2}px)`,\n                      width: thick,\n                      height: len,\n                    }\n                  : {\n                      bottom: m * CHASE_INSET_F - thick * 0.4,\n                      left: `calc(${along * 100}% - ${len / 2}px)`,\n                      height: thick,\n                      width: len,\n                    }\n              }\n            >\n              <div\n                className=\"absolute inset-0 [--wedge-a:46%] dark:[--wedge-a:58%]\"\n                style={{\n                  background: \"color-mix(in srgb, var(--background), var(--foreground) var(--wedge-a))\",\n                  transform: edge === \"right\" ? \"skewY(-14deg)\" : \"skewX(-14deg)\",\n                }}\n              />\n              <div\n                className=\"absolute inset-0 [--wedge-b:34%] dark:[--wedge-b:46%]\"\n                style={{\n                  background: \"color-mix(in srgb, var(--background), var(--foreground) var(--wedge-b))\",\n                  transform:\n                    edge === \"right\"\n                      ? \"skewY(-14deg) translateY(var(--travel))\"\n                      : \"skewX(-14deg) translateX(var(--travel))\",\n                }}\n              />\n            </div>\n          );\n        })}\n        <div\n          ref={keyRef}\n          className=\"absolute h-[1.6em] w-[1.6em] max-h-4 max-w-4 border-2 border-ns-muted transition-[opacity] duration-200\"\n          style={{\n            right: m * CHASE_INSET_F - 8,\n            bottom: `calc(20% + var(--key-slot, 0) * 15%)`,\n            transform: \"rotate(var(--key-rotate, 0deg))\",\n            opacity: 0,\n          }}\n        />\n        <div\n          ref={planerRef}\n          className=\"absolute top-0 h-full w-[3px] bg-ns-muted transition-[opacity] duration-150\"\n          style={{ left: \"var(--planer-x, 0%)\", opacity: 0 }}\n        />\n      </div>\n\n      <div\n        ref={gridRef}\n        role=\"group\"\n        aria-label=\"Bento grid locked by four quoins. Arrow keys move between tiles.\"\n        className=\"relative z-[1] grid h-full w-full gap-0\"\n        style={{\n          padding: m * CHASE_INSET_F + Math.max(1, m * RAIL_STROKE_F) * 2,\n          gridTemplateColumns: compact ? compactTemplateCols : fullTemplateCols,\n          gridTemplateRows: compact ? compactTemplateRows : fullTemplateRows,\n        }}\n      >\n        {gutterKeys.map((key) => (\n          <div\n            key={key}\n            aria-hidden\n            className=\"[--fill-a:40%]\"\n            style={{\n              gridColumn: gridAreaFor(key, compact, \"col\"),\n              gridRow: gridAreaFor(key, compact, \"row\"),\n              backgroundColor: \"color-mix(in srgb, var(--background), var(--foreground) var(--fill-a))\",\n              backgroundImage:\n                key.startsWith(\"gx\")\n                  ? `repeating-linear-gradient(to bottom, transparent 0, transparent calc(${Math.max(\n                      1,\n                      m * 0.11\n                    )}px - 1px), color-mix(in srgb, var(--border), var(--foreground) 30%) calc(${Math.max(\n                      1,\n                      m * 0.11\n                    )}px - 1px), color-mix(in srgb, var(--border), var(--foreground) 30%) ${Math.max(1, m * 0.11)}px)`\n                  : `repeating-linear-gradient(to right, transparent 0, transparent calc(${Math.max(\n                      1,\n                      m * 0.11\n                    )}px - 1px), color-mix(in srgb, var(--border), var(--foreground) 30%) calc(${Math.max(\n                      1,\n                      m * 0.11\n                    )}px - 1px), color-mix(in srgb, var(--border), var(--foreground) 30%) ${Math.max(1, m * 0.11)}px)`,\n            }}\n          />\n        ))}\n\n        {rows.map((tile, i) => {\n          const isHero = !compact && i === 0;\n          const commonProps = {\n            ref: (el: HTMLAnchorElement | HTMLButtonElement | null) => {\n              if (el) tileRefs.current.set(tile.id, el);\n              else tileRefs.current.delete(tile.id);\n            },\n            tabIndex: i === activeIdx ? 0 : -1,\n            onFocus: () => setActiveIdx(i),\n            onMouseEnter: () => setHoveredId(tile.id),\n            onMouseLeave: () => setHoveredId((cur) => (cur === tile.id ? null : cur)),\n            onKeyDown: (e: KeyboardEvent<HTMLElement>) => onKeyDown(e, i),\n            className:\n              \"quoin-lockup-tile group relative isolate flex flex-col justify-start gap-1 overflow-hidden rounded-sm border p-3 text-left transition-colors duration-150 \" +\n              \"[--face-mix:10%] dark:[--face-mix:16%] \" +\n              \"border-transparent hover:[--face-mix:12%] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent\",\n            style: {\n              gridColumn: gridAreaFor(`tile-${i}`, compact, \"col\"),\n              gridRow: gridAreaFor(`tile-${i}`, compact, \"row\"),\n              backgroundColor: \"color-mix(in srgb, var(--background), var(--foreground) var(--face-mix))\",\n              borderTopColor: \"color-mix(in srgb, var(--foreground) calc(var(--edge-o, 1) * 22%), transparent)\",\n              transform:\n                \"translateY(var(--sink, 0px)) scale(var(--tile-scale, 1)) rotate(var(--tile-tilt, 0deg))\",\n              boxShadow: \"0 calc(var(--sink, 0px) * 0.6) calc(var(--sink, 0px) * 1.8) color-mix(in srgb, var(--foreground) calc(var(--shadow-o, 0) * 38%), transparent)\",\n              zIndex: i,\n            } as React.CSSProperties,\n            \"aria-label\": tile.title,\n            \"data-tile-id\": tile.id,\n          };\n          const content = (\n            <>\n              <h3 className={`font-medium leading-snug text-foreground ${isHero ? \"text-base\" : \"text-sm\"}`}>\n                {tile.title}\n              </h3>\n              {tile.body && (\n                <p className={`text-ns-muted ${isHero ? \"max-w-[34ch] text-sm leading-snug\" : \"text-xs leading-snug\"}`}>\n                  {tile.body}\n                </p>\n              )}\n            </>\n          );\n          return tile.href ? (\n            <a key={tile.id} {...commonProps} href={tile.href}>\n              {content}\n            </a>\n          ) : (\n            <button key={tile.id} {...commonProps} type=\"button\">\n              {content}\n            </button>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n\n// Raw grid line coordinates for the fixed arrangement. Content track i (1..n)\n// sits at raw line [2i-1, 2i] once gutter tracks are interleaved between\n// every pair of content tracks. These are constants — never recomputed by\n// pressure state, only read once per render.\nfunction gridAreaFor(key: string, compact: boolean, axis: \"col\" | \"row\"): string {\n  const rawLine = (content: number) => `${2 * content - 1} / ${2 * content + 1}`;\n  if (compact) {\n    const colOf = (c1: number, c2 = c1) => `${2 * c1 - 1} / ${2 * c2}`;\n    const rowOf = (r1: number, r2 = r1) => `${2 * r1 - 1} / ${2 * r2}`;\n    if (key === \"gx1\") return axis === \"col\" ? \"2 / 3\" : \"1 / 4\";\n    if (key === \"gy1\") return axis === \"row\" ? \"2 / 3\" : \"1 / 4\";\n    switch (key) {\n      case \"tile-0\": // wide: cols 1-2, row 1\n        return axis === \"col\" ? colOf(1, 2) : rowOf(1);\n      case \"tile-1\": // a: col 1, row 2\n        return axis === \"col\" ? colOf(1) : rowOf(2);\n      case \"tile-2\": // b: col 2, row 2\n        return axis === \"col\" ? colOf(2) : rowOf(2);\n      default:\n        return \"1 / 2\";\n    }\n  }\n  const colOf = (c1: number, c2 = c1) => `${2 * c1 - 1} / ${2 * c2}`;\n  const rowOf = (r1: number, r2 = r1) => `${2 * r1 - 1} / ${2 * r2}`;\n  if (key === \"gx1\") return axis === \"col\" ? \"2 / 3\" : \"1 / 7\";\n  if (key === \"gx2\") return axis === \"col\" ? \"4 / 5\" : \"1 / 6\";\n  if (key === \"gy1\") return axis === \"row\" ? \"2 / 3\" : \"1 / 6\";\n  if (key === \"gy2\") return axis === \"row\" ? \"4 / 5\" : \"1 / 6\";\n  if (key === \"gy3\") return axis === \"row\" ? \"6 / 7\" : \"1 / 6\";\n  switch (key) {\n    case \"tile-0\": // hero: cols 1-2, rows 1-2\n      return axis === \"col\" ? colOf(1, 2) : rowOf(1, 2);\n    case \"tile-1\": // tall: col 3, rows 1-2\n      return axis === \"col\" ? colOf(3) : rowOf(1, 2);\n    case \"tile-2\": // wide: cols 1-2, row 3\n      return axis === \"col\" ? colOf(1, 2) : rowOf(3);\n    case \"tile-3\": // a: col 3, row 3\n      return axis === \"col\" ? colOf(3) : rowOf(3);\n    case \"tile-4\": // b: cols 1-3, row 4 — the foot course runs the full width,\n      // so the chase is packed edge to edge and no cell is left inert\n      return axis === \"col\" ? colOf(1, 3) : rowOf(4);\n    default:\n      return rawLine(1);\n  }\n}\n\nQuoinLockup.displayName = \"QuoinLockup\";\n",
      "type": "registry:ui",
      "target": "components/ui/quoin-lockup.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": [
      "grid",
      "layout",
      "bento",
      "letterpress",
      "print",
      "keyboard-navigation",
      "accessibility"
    ],
    "instruction": "Build <QuoinLockup tiles? planerEvery? className?> — a bento grid borrowed from letterpress lockup on the imposing stone, where the interesting variable is PRESSURE distributing a layout that never repacks (the line separating this from grid-bento-dense/grid-bento-ascii, both re-assignment problems). FIXED ARRANGEMENT: 5 tiles on a 3-content-column x 4-content-row track grid — hero 2x2 (cols1-2,rows1-2), one 1x2 tall (col3,rows1-2), one 2x1 wide (cols1-2,row3), two 1x1 (col3 row3; col1 row4) — with the one leftover cell (cols2-3,row4) rendered as a plain furniture block, no tile: the real process packs every empty area with furniture rather than leaving it inert. A literal 3x3 (9 cells) cannot hold this tile set: 2x2 + 1x2 + 1x2 + 1x1 + 1x1 = 4+2+2+1+1 = 10 cells of area, one more than 3x3 provides, so the base grid is extended to 3x4 (12 cells) rather than dropping a tile's area to force the fit. Below M=320px (M = min(width,height)) this collapses to 3 tiles on a 2x2 base (one 2x1 wide + two 1x1). Grid tracks interleave CONTENT/GUTTER/CONTENT on both axes; every grid-column/grid-row span is a module-level constant written once — pressure only ever changes gutter TRACK SIZE via CSS custom properties (--gw-gx1/gx2/gy1/gy2/gy3) set imperatively on the grid container in a single rAF loop, never React state, so a 60fps decay never re-renders the tree. PRESSURE MODEL, a pure function of absolute elapsed time (never integrated dt, so any instant reproduces byte-identically): P decays from 0.93 to 0.62 at 0.055/s (~5.64s). At P<0.62's lift threshold a re-key phase runs: 4 quoins turn in fixed order (right-top, foot-right, right-bottom, foot-left) 640ms apart, each rotating its key 90 degrees over 320ms and stepping P by +0.19 (clamped at 0.93) over the turn's final 180ms, critically damped (smoothstep here). Full cycle ~8.2s (5.64s decay + 4*640ms=2.56s re-key), unbounded, never terminating. Every `planerEvery` cycles (default 3, every ~24.6s) a planer bar sweeps left-to-right across the form during the re-key phase's first 700ms, forcibly zeroing each tile's sink the instant the bar's x-progress passes that tile's x-fraction — the component's striking moment. FURNITURE: gutter width in u = 0.0125*M ('one pica') is quantised to the nearest of {2,3,4,6,8}*u from a target that widens linearly as P falls — never a continuous value — rendered as a color-mix(in srgb, var(--background), var(--foreground) 40%) fill with a repeating-linear-gradient butt-joint line every 0.11*M using color-mix(var(--border), var(--foreground) 30%). PIEING: each tile has a fixed seeded hold H_i = P + 0.10*JITTER[i] (JITTER = [-0.62, 0.35, -0.88, 0.71, 0.08], hardcoded, never Math.random, so the same tile always loosens first); when H_i<0.62 the tile sinks translateY up to 0.028*M, scales to 0.978 and tilts 0.9deg about a fixed per-tile axis, gated entirely by CSS custom properties (--sink/--tile-scale/--tile-tilt) so the transform composites without layout thrash. Pie depth carries two independent value cues: a box-shadow toward --foreground scaling with sink fraction (a literal loss of local contrast under the tile, not a colour), and a border-top toward color-mix(var(--foreground) 22%) that fades as sink increases (loss of edge contrast against the still-flush neighbours) — deliberately NOT the spec's literal mix(bg,fg,0.10) hairline, which sits within 1% of the tile face itself and is invisible; the edge is pushed to ~22% foreground so it has contrast to lose in the first place. QUOINS: 4 opposing-wedge pairs (2 right edge, 2 foot), each a pair of divs skewed 14 degrees (skewY on the right edge, skewX on the foot) at two different color-mix stops so the direction of travel reads without hue; the sliding half translates by up to 1.0u, driven by the same rAF loop. A small bordered key marker docks at whichever quoin is mid-turn, rotating 0-90deg over its 320ms window, opacity 0 outside the re-key phase. INTERACTION: hovering or focusing a tile steps its 1-2 adjacent gutters down one legal size (a hardcoded per-tile adjacency table, e.g. the hero borders {gx2,gy2}) — the tile's own span never changes, only the furniture around it narrows, and the creep/re-key loop keeps running underneath. Tiles are real <a>/<button> elements with accessible names; roving tabindex plus ArrowLeft/Right/Up/Down cycle through the fixed tile order (never DOM reorder, since the arrangement never changes, focus order is the visual order by construction). COLOUR: everything is CSS var()/color-mix() consumed directly by Tailwind classes and inline styles — a `dark:[--x:Y%]` class flips a small custom property that a color-mix formula reads, so there is no getComputedStyle/MutationObserver pipeline and nothing can paint before a token resolves, because nothing ever reads a token in JS; matches grid-bento-dense, the nearest sibling in this collection. `prefers-reduced-motion` calls the same pure state(t) function once at STATIC_TIME=6.3s (mid re-key: some quoins keyed, one tile still pied, the key sitting in a socket) and never starts the rAF loop; IntersectionObserver and visibilitychange both stop it otherwise. M is measured in useLayoutEffect via ResizeObserver on the host so no frame ever paints the form at the wrong size."
  },
  "type": "registry:ui"
}