{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tiling",
  "title": "Tiling",
  "description": "Tiles on the 12-column grid, held apart by space and one hairline, never boxed. Leave the sheet still, or drag a tile and pull its corner: the grid makes room for your arrangement.",
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/tiling.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype TilingProps = React.ComponentProps<\"div\"> & {\n  /** rules: one hairline between tiles, as \"Less is more.\" divides its sheet. crosses: no lines, a registration cross where the tiles' corners meet. */\n  variant?: \"rules\" | \"crosses\"\n}\n\n/**\n * Tiles on the 12-column grid, held apart by space and one hairline, never boxed. A tiling covers the plane with\n * no gaps and no overlaps, so neighbours share an edge: each tile draws only its start and top edge, halfway into\n * the gap, and the edges that meet the tiling's own border are clipped away.\n */\nfunction Tiling({ variant = \"rules\", className, children, ...props }: TilingProps) {\n  // The sheet is the grid; the outer box is the container its narrow layouts are measured against.\n  return (\n    <div data-slot=\"tiling\" data-variant={variant} className={cn(\"ot-tiling\", className)} {...props}>\n      <div className=\"ot-tiling-sheet\">{children}</div>\n    </div>\n  )\n}\n\ntype TileProps = React.ComponentProps<\"div\"> & {\n  /** Columns out of 12. In a narrow tiling a tile of 6 or fewer takes half, a wider one the whole width. */\n  span?: number\n  /** Rows it stands through. */\n  rows?: number\n  /** The corner its content keeps to, as the poster keeps its notes. A child with margin-block-start: auto goes to the foot. */\n  place?: \"start-top\" | \"end-top\" | \"start-foot\" | \"end-foot\"\n}\n\nfunction Tile({ span = 4, rows = 1, place = \"start-top\", className, style, ...props }: TileProps) {\n  return (\n    <div\n      data-slot=\"tile\"\n      data-place={place}\n      className={cn(\"ot-tile\", className)}\n      style={{ \"--span\": span, \"--rows\": rows, ...style } as React.CSSProperties}\n      {...props}\n    />\n  )\n}\n\n/** A plain, serialisable rectangle. Columns and rows start at 1, from the inline start. */\ntype TilingItem = {\n  id: string\n  label: string\n  column: number\n  row: number\n  span: number\n  rows: number\n}\n\ntype TilingLayout = readonly TilingItem[]\n\ntype TilingEditorProps = Omit<React.ComponentProps<\"div\">, \"children\" | \"defaultValue\"> & {\n  label: string\n  value?: TilingLayout\n  defaultValue?: TilingLayout\n  onValueChange?: (value: TilingItem[]) => void\n  variant?: TilingProps[\"variant\"]\n  renderTile?: (tile: TilingItem) => React.ReactNode\n  /** Offers JSON on the page as well as a clipboard action. */\n  copyLayout?: boolean\n  /** Starts with a tile picked up; useful for a holding specimen. */\n  defaultHeld?: string\n}\n\nconst overlaps = (a: TilingItem, b: TilingItem) => a.column < b.column + b.span && b.column < a.column + a.span && a.row < b.row + b.rows && b.row < a.row + a.rows\nconst geometry = (tile: TilingItem) => `column ${tile.column}, row ${tile.row}. ${tile.span} wide, ${tile.rows} high.`\nconst copyTiles = (tiles: TilingLayout) => tiles.map((tile) => ({ ...tile }))\n\nfunction layoutError(tiles: TilingLayout) {\n  const ids = new Set<string>()\n  for (const tile of tiles) {\n    if (!tile.id || ids.has(tile.id)) return \"Give every tile a unique, non-empty id.\"\n    ids.add(tile.id)\n    if (typeof tile.label !== \"string\" || !tile.label.trim()) return \"Give every tile a label.\"\n    if (![tile.column, tile.row, tile.span, tile.rows].every((n) => Number.isSafeInteger(n) && n > 0) || tile.column + tile.span > 13 || !Number.isSafeInteger(tile.row + tile.rows)) return \"Use positive whole rows and spans, within the 12 columns.\"\n  }\n  for (let i = 0; i < tiles.length; i++) {\n    if (tiles.slice(i + 1).some((tile) => overlaps(tiles[i], tile))) return \"Place tiles in separate spaces; two tiles overlap.\"\n  }\n  return null\n}\n\ntype EditKind = \"move\" | \"corner\" | \"start\" | \"end\" | \"top\" | \"bottom\"\ntype TilingEdit = { id: string; base: TilingLayout; layout: TilingItem[]; kind: EditKind; offset?: { x: number; y: number } }\nconst sameGeometry = (a: TilingItem, b: TilingItem) => a.column === b.column && a.row === b.row && a.span === b.span && a.rows === b.rows\nconst tracks = (tile: TilingItem) => ({ gridColumn: `${tile.column} / span ${tile.span}`, gridRow: `${tile.row} / span ${tile.rows}` })\n\n// A sparse sheet can make room. Matching rectangles swap; other neighbours keep their\n// columns and move down, in reading order, until each has its own unoccupied rectangle.\nfunction arrange(tiles: TilingLayout, next: TilingItem, kind: EditKind) {\n  const original = tiles.find((tile) => tile.id === next.id)!\n  const neighbours = tiles.filter((tile) => tile.id !== next.id)\n  const hit = neighbours.filter((tile) => overlaps(next, tile))\n  if (kind === \"move\" && hit.length === 1 && sameGeometry(next, { ...hit[0], id: next.id })) {\n    const swapped = { ...hit[0], column: original.column, row: original.row, span: original.span, rows: original.rows }\n    return tiles.map((tile) => tile.id === next.id ? next : tile.id === swapped.id ? swapped : { ...tile })\n  }\n  const placed = [next]\n  for (const tile of [...neighbours].sort((a, b) => a.row - b.row || a.column - b.column)) {\n    let candidate = { ...tile }\n    let collisions = placed.filter((other) => overlaps(candidate, other))\n    while (collisions.length) {\n      candidate = { ...candidate, row: Math.max(...collisions.map((other) => other.row + other.rows)) }\n      collisions = placed.filter((other) => overlaps(candidate, other))\n    }\n    placed.push(candidate)\n  }\n  return tiles.map((tile) => placed.find((other) => other.id === tile.id)!)\n}\n\nfunction adjusted(tile: TilingItem, dx: number, dy: number, kind: EditKind) {\n  const next = { ...tile }\n  if (kind === \"move\") {\n    next.column = Math.max(1, Math.min(13 - tile.span, tile.column + dx))\n    next.row = Math.max(1, tile.row + dy)\n  } else {\n    if (kind === \"corner\" || kind === \"end\") next.span = Math.max(1, Math.min(13 - tile.column, tile.span + dx))\n    if (kind === \"corner\" || kind === \"bottom\") next.rows = Math.max(1, tile.rows + dy)\n    if (kind === \"start\") {\n      next.column = Math.max(1, Math.min(tile.column + tile.span - 1, tile.column + dx))\n      next.span = tile.span + tile.column - next.column\n    }\n    if (kind === \"top\") {\n      next.row = Math.max(1, Math.min(tile.row + tile.rows - 1, tile.row + dy))\n      next.rows = tile.rows + tile.row - next.row\n    }\n  }\n  return next\n}\n\n/** Editing is opt-in. Pointer and keyboard previews commit together on drop; Escape discards them. */\nfunction TilingEditor({ label, value, defaultValue = [], onValueChange, variant = \"rules\", renderTile, copyLayout = false, defaultHeld, className, ...props }: TilingEditorProps) {\n  const [local, setLocal] = React.useState<TilingLayout>(() => copyTiles(defaultValue))\n  const tiles = value ?? local\n  const error = layoutError(tiles)\n  const [selected, setSelected] = React.useState(defaultHeld ?? \"\")\n  const [edit, setEdit] = React.useState<TilingEdit | null>(() => defaultHeld && tiles.some((tile) => tile.id === defaultHeld) ? { id: defaultHeld, base: tiles, layout: copyTiles(tiles), kind: \"move\" } : null)\n  // An external controlled update always wins over an unfinished gesture.\n  const activeEdit = edit?.base === tiles ? edit : null\n  const shown = activeEdit?.layout ?? tiles\n  const current = shown.find((tile) => tile.id === selected)\n  const [notice, setNotice] = React.useState({ text: \"\", sequence: 0 })\n  const hint = React.useId()\n  const keys = React.useId()\n  const sheet = React.useRef<HTMLDivElement>(null)\n  const add = React.useRef<HTMLButtonElement>(null)\n  const output = React.useRef<HTMLDetailsElement>(null)\n  const json = React.useRef<HTMLTextAreaElement>(null)\n  const handles = React.useRef(new Map<string, HTMLDivElement>())\n  const focusNext = React.useRef<string | null>(null)\n  const previous = React.useRef(new Map<string, { x: number; y: number }>())\n  const animateNext = React.useRef(false)\n  const suppressClick = React.useRef(false)\n  const drag = React.useRef<{\n    tile: TilingItem; base: TilingLayout; pointer: number; x: number; y: number\n    columnStep: number; rowStep: number; rtl: boolean; kind: EditKind; moved: boolean; preview: TilingItem[]\n  } | null>(null)\n\n  // Before capture, the release can land anywhere. A lost window also ends the gesture.\n  React.useEffect(() => {\n    const abandon = () => {\n      const pending = drag.current\n      drag.current = null\n      if (!pending?.moved) return\n      animateNext.current = true\n      setEdit(null)\n      setNotice((before) => ({ text: \"Edit cancelled. Saved layout unchanged.\", sequence: before.sequence + 1 }))\n      const handle = handles.current.get(pending.tile.id)\n      if (handle?.hasPointerCapture(pending.pointer)) handle.releasePointerCapture(pending.pointer)\n    }\n    const end = (event: PointerEvent) => {\n      const pending = drag.current\n      if (pending?.pointer === event.pointerId && (event.type === \"pointercancel\" || !pending.moved)) abandon()\n    }\n    document.addEventListener(\"pointerup\", end, true)\n    document.addEventListener(\"pointercancel\", end, true)\n    window.addEventListener(\"blur\", abandon)\n    return () => {\n      document.removeEventListener(\"pointerup\", end, true)\n      document.removeEventListener(\"pointercancel\", end, true)\n      window.removeEventListener(\"blur\", abandon)\n    }\n  }, [])\n\n  const say = (text: string) => setNotice((before) => ({ text, sequence: before.sequence + 1 }))\n  const commit = (next: TilingItem[]) => {\n    animateNext.current = true\n    if (value === undefined) setLocal(next)\n    onValueChange?.(copyTiles(next))\n  }\n  const lift = (tile: TilingItem, kind: EditKind = \"move\") => {\n    setSelected(tile.id)\n    setEdit({ id: tile.id, base: tiles, layout: copyTiles(tiles), kind })\n    say(`${tile.label} picked up. ${geometry(tile)}`)\n  }\n  const clear = () => {\n    drag.current = null\n    setEdit(null)\n    animateNext.current = true\n  }\n  const cancel = () => {\n    clear()\n    say(\"Edit cancelled. Saved layout unchanged.\")\n  }\n  const drop = (next = activeEdit?.layout) => {\n    const tile = next?.find((item) => item.id === selected)\n    if (next && next.some((item, i) => !sameGeometry(item, tiles[i]))) commit(next)\n    clear()\n    if (tile) say(`${tile.label} set down. ${geometry(tile)}`)\n  }\n\n  // Capture visible positions, including the held tile's pointer offset, so release\n  // glides from the hand to its landing cells. Prop updates and mounts stay still.\n  React.useLayoutEffect(() => {\n    const board = sheet.current\n    if (!board) return\n    const bounds = board.getBoundingClientRect()\n    const cs = getComputedStyle(board)\n    const scaleX = bounds.width / board.offsetWidth || 1\n    const scaleY = bounds.height / board.offsetHeight || 1\n    const glide = animateNext.current && !matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n    const seen = new Map<string, { x: number; y: number }>()\n    Array.from(board.children).forEach((node) => {\n      if (!(node instanceof HTMLElement) || !node.dataset.id) return\n      const rect = node.getBoundingClientRect()\n      const at = { x: (rect.left - bounds.left) / scaleX, y: (rect.top - bounds.top) / scaleY }\n      const before = previous.current.get(node.dataset.id)\n      seen.set(node.dataset.id, at)\n      if (glide && before && !node.hasAttribute(\"data-dragging\") && (Math.abs(at.x - before.x) > 0.5 || Math.abs(at.y - before.y) > 0.5)) {\n        node.getAnimations().forEach((animation) => animation.cancel())\n        node.animate([{ translate: `${before.x - node.offsetLeft}px ${before.y - node.offsetTop}px` }, { translate: \"0 0\" }], { duration: parseFloat(cs.getPropertyValue(\"--ot-moderato\")) || 320, easing: cs.getPropertyValue(\"--ot-breath\").trim() || \"ease\" })\n      }\n    })\n    previous.current = seen\n    animateNext.current = false\n    if (focusNext.current) {\n      handles.current.get(focusNext.current)?.focus({ preventScroll: true })\n      focusNext.current = null\n    }\n  })\n\n  const key = (event: React.KeyboardEvent<HTMLElement>, tile: TilingItem, kind: EditKind = \"move\") => {\n    if (event.target !== event.currentTarget || event.altKey || event.ctrlKey || event.metaKey) return\n    if (event.key === \"Enter\" || event.key === \" \") {\n      event.preventDefault()\n      event.stopPropagation()\n      if (activeEdit?.id === tile.id) drop()\n      else lift(tile, kind)\n      return\n    }\n    if (!activeEdit || activeEdit.id !== tile.id) return\n    const rtl = getComputedStyle(event.currentTarget).direction === \"rtl\"\n    const dx = (event.key === \"ArrowRight\" ? 1 : event.key === \"ArrowLeft\" ? -1 : 0) * (rtl ? -1 : 1)\n    const dy = event.key === \"ArrowDown\" ? 1 : event.key === \"ArrowUp\" ? -1 : 0\n    if (!dx && !dy) return\n    event.preventDefault()\n    event.stopPropagation()\n    const action = event.shiftKey ? \"corner\" : kind\n    const next = adjusted(tile, dx, dy, action)\n    if (sameGeometry(next, tile)) { say(`${tile.label} reached the grid edge.`); return }\n    const layout = arrange(activeEdit.base, next, action)\n    animateNext.current = true\n    setEdit({ ...activeEdit, layout, kind: action })\n    say(`${tile.label} ${action === \"move\" ? \"moved\" : \"resized\"}. ${geometry(next)} Neighbours make room.`)\n  }\n\n  const pointerDown = (event: React.PointerEvent<HTMLElement>, tile: TilingItem, kind: EditKind = \"move\") => {\n    if (event.defaultPrevented || event.button !== 0 || !event.isPrimary || !sheet.current) return\n    if ((event.target as HTMLElement).closest(\"input, textarea, select, [contenteditable]:not([contenteditable='false']), [data-tiling-no-drag]\")) return\n    if (kind !== \"move\") { event.preventDefault(); event.stopPropagation() }\n    const board = sheet.current\n    const bounds = board.getBoundingClientRect()\n    const cs = getComputedStyle(board)\n    const scaleX = bounds.width / board.offsetWidth || 1\n    const scaleY = bounds.height / board.offsetHeight || 1\n    suppressClick.current = false\n    // Do not capture or prevent the initial press: links and buttons still receive a click.\n    drag.current = { tile, base: tiles, pointer: event.pointerId, x: (event.clientX - bounds.left) / scaleX, y: (event.clientY - bounds.top) / scaleY, columnStep: (board.offsetWidth + parseFloat(cs.columnGap)) / 12, rowStep: parseFloat(cs.gridAutoRows) + parseFloat(cs.rowGap), rtl: cs.direction === \"rtl\", kind, moved: false, preview: copyTiles(tiles) }\n    setSelected(tile.id)\n  }\n  const pointerMove = (event: React.PointerEvent<HTMLDivElement>) => {\n    const pending = drag.current\n    if (!pending || pending.pointer !== event.pointerId || !sheet.current) return\n    if (!(event.buttons & 1)) { cancel(); return }\n    if (pending.base !== tiles) { cancel(); return }\n    const board = sheet.current\n    const bounds = board.getBoundingClientRect()\n    const scaleX = bounds.width / board.offsetWidth || 1\n    const scaleY = bounds.height / board.offsetHeight || 1\n    const x = (event.clientX - bounds.left) / scaleX - pending.x\n    const y = (event.clientY - bounds.top) / scaleY - pending.y\n    // Translations and grid steps are local; the click threshold stays in viewport pixels.\n    if (!pending.moved && Math.hypot(x * scaleX, y * scaleY) < 6) return\n    if (!pending.moved) {\n      pending.moved = true\n      event.currentTarget.focus({ preventScroll: true })\n      event.currentTarget.setPointerCapture(event.pointerId)\n      event.currentTarget.getAnimations().forEach((animation) => animation.cancel())\n    }\n    event.preventDefault()\n    suppressClick.current = true\n    const dx = Math.round(x / pending.columnStep) * (pending.rtl ? -1 : 1)\n    const dy = Math.round(y / pending.rowStep)\n    const next = adjusted(pending.tile, dx, dy, pending.kind)\n    const layout = arrange(pending.base, next, pending.kind)\n    const was = pending.preview.find((tile) => tile.id === next.id)!\n    animateNext.current = !sameGeometry(was, next)\n    if (!sameGeometry(was, next)) say(`${next.label}, ${geometry(next)} Release to place. Neighbours make room.`)\n    pending.preview = layout\n    setEdit({ id: next.id, base: pending.base, layout, kind: pending.kind, offset: pending.kind === \"move\" ? { x, y } : undefined })\n  }\n  const pointerUp = (event: React.PointerEvent<HTMLDivElement>) => {\n    const pending = drag.current\n    if (!pending || pending.pointer !== event.pointerId) return\n    if (pending.base !== tiles) cancel()\n    else if (pending.moved) drop(pending.preview)\n    else drag.current = null\n    if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId)\n  }\n\n  const addTile = () => {\n    let n = tiles.length + 1\n    while (tiles.some((tile) => tile.id === `tile-${n}`)) n++\n    const tile = { id: `tile-${n}`, label: `Tile ${n}`, column: 1, row: 1, span: 4, rows: 1 }\n    const last = Math.max(0, ...tiles.map((item) => item.row + item.rows - 1)) + 1\n    for (let row = 1; row <= last; row++) {\n      for (let column = 1; column <= 9; column++) {\n        Object.assign(tile, { column, row })\n        if (tiles.some((item) => overlaps(tile, item))) continue\n        clear()\n        focusNext.current = tile.id\n        commit([...tiles, tile])\n        setSelected(tile.id)\n        say(`${tile.label} added. ${geometry(tile)}`)\n        return\n      }\n    }\n  }\n  const removeTile = () => {\n    if (!current) return\n    const at = tiles.findIndex((tile) => tile.id === current.id)\n    const next = tiles.filter((tile) => tile.id !== current.id)\n    focusNext.current = next[Math.min(at, next.length - 1)]?.id ?? null\n    clear()\n    commit(next)\n    setSelected(focusNext.current ?? \"\")\n    if (!next.length) add.current?.focus()\n    say(`${current.label} removed. ${next.length} ${next.length === 1 ? \"tile remains\" : \"tiles remain\"}.`)\n  }\n  const copy = async () => {\n    try {\n      await navigator.clipboard.writeText(JSON.stringify(tiles, null, 2))\n      say(\"Layout copied as JSON.\")\n    } catch {\n      if (output.current) output.current.open = true\n      json.current?.focus()\n      json.current?.select()\n      say(\"Clipboard unavailable. Select and copy the layout below.\")\n    }\n  }\n\n  const rows = error ? 2 : Math.max(2, ...shown.map((tile) => tile.row + tile.rows), ...tiles.map((tile) => tile.row + tile.rows))\n  const landing = activeEdit?.layout.find((tile) => tile.id === activeEdit.id)\n  return (\n    <div data-slot=\"tiling-editor\" data-variant={variant} className={cn(\"ot-tiling ot-tiling-editor\", className)} {...props} onKeyDown={(event) => { if (event.key === \"Escape\" && (activeEdit || drag.current)) { event.preventDefault(); cancel() }; props.onKeyDown?.(event) }}>\n      <div className=\"ot-tiling-toolbar\">\n        <span className=\"ot-tiling-caption\">{label}</span>\n        <div className=\"ot-tiling-actions\" role=\"group\" aria-label={`${label} actions`}>\n          <button ref={add} type=\"button\" onClick={addTile} disabled={!!error}>Add tile</button>\n          <button type=\"button\" onClick={removeTile} disabled={!current || !!error} aria-label={current ? `Remove tile: ${current.label}` : \"Remove tile\"}>Remove tile</button>\n          {copyLayout ? <button type=\"button\" onClick={copy} disabled={!!error}>Copy layout</button> : null}\n        </div>\n      </div>\n      <div className=\"ot-tiling-instructions\">\n        <p id={hint} className=\"ot-tiling-hint\">Drag to move. Drag the corner to resize.</p>\n        <p id={keys} className=\"ot-tiling-keys\">Enter / Space picks up or drops. Arrows move. Shift + arrows resize. Escape cancels.</p>\n      </div>\n      {error ? <p role=\"alert\">{error}</p> : (\n        <div className=\"ot-tiling-viewport\" tabIndex={0} role=\"region\" aria-label={`${label}, 12-column editing sheet`} aria-describedby={`${hint} ${keys}`}>\n          <div className=\"ot-tiling-canvas\">\n            <div className=\"ot-tiling-columns\" aria-hidden=\"true\">{Array.from({ length: 12 }, (_, i) => <span key={i}><bdi>{String(i + 1).padStart(2, \"0\")}</bdi></span>)}</div>\n            <div ref={sheet} className=\"ot-tiling-sheet\" style={{ \"--ot-tiling-rows\": rows } as React.CSSProperties}>\n              {landing ? <div className=\"ot-tiling-landing\" aria-hidden=\"true\" style={tracks(landing)} /> : null}\n              {shown.map((tile) => {\n                const holding = activeEdit?.id === tile.id\n                const floating = holding && activeEdit.offset\n                const rectangle = floating ? activeEdit.base.find((item) => item.id === tile.id)! : tile\n                return (\n                  <Tile key={tile.id} ref={(node) => { if (node) handles.current.set(tile.id, node); else handles.current.delete(tile.id) }} span={rectangle.span} rows={rectangle.rows} data-id={tile.id} data-selected={selected === tile.id || undefined} data-held={holding || undefined} data-dragging={!!floating || undefined} role=\"group\" aria-roledescription=\"movable tile\" aria-label={`${tile.label}, ${geometry(tile)}`} aria-describedby={`${hint} ${keys}`} tabIndex={0} style={{ ...tracks(rectangle), translate: floating ? `${floating.x}px ${floating.y}px` : undefined }}\n                    onKeyDown={(event) => key(event, tile)} onPointerDown={(event) => pointerDown(event, tile)} onPointerMove={pointerMove} onPointerUp={pointerUp}\n                    onPointerCancel={cancel} onLostPointerCapture={(event) => { if (event.target === event.currentTarget && drag.current?.moved) cancel() }} onDragStart={(event) => event.preventDefault()}\n                    onClickCapture={(event) => { if (suppressClick.current) { event.preventDefault(); event.stopPropagation(); suppressClick.current = false } }}\n                    onClick={() => setSelected(tile.id)}>\n                    <button type=\"button\" className=\"ot-tiling-move\" tabIndex={-1} aria-label={`Move ${tile.label}`} aria-pressed={holding} aria-describedby={`${hint} ${keys}`}\n                      onKeyDown={(event) => key(event, tile)} onClick={(event) => { if (event.detail === 0) { if (holding) drop(); else lift(tile) } }} />\n                    <div className=\"ot-tiling-content\">{renderTile ? renderTile(tile) : <span className=\"ot-tiling-name\">{tile.label}</span>}</div>\n                    {holding ? <span className=\"ot-tiling-dimensions\" aria-hidden=\"true\"><span>{tile.span}</span><span> × </span><span>{tile.rows}</span></span> : null}\n                    {([\"start\", \"end\", \"top\", \"bottom\", \"corner\"] as const).map((side) => (\n                      <button key={side} type=\"button\" className=\"ot-tiling-size\" data-side={side} aria-label={`Resize ${tile.label}${side === \"corner\" ? \"\" : ` ${side} edge`}`} aria-describedby={`${hint} ${keys}`} aria-pressed={holding}\n                        onKeyDown={(event) => key(event, tile, side)} onPointerDown={(event) => pointerDown(event, tile, side)}\n                        onClick={(event) => { event.stopPropagation(); if (event.detail === 0) { if (holding) drop(); else lift(tile, side) } }} />\n                    ))}\n                  </Tile>\n                )\n              })}\n              {!tiles.length ? <p className=\"ot-tiling-empty\">A little space.<br /><span>Add your first tile.</span></p> : null}\n            </div>\n          </div>\n        </div>\n      )}\n      <p className=\"ot-tiling-status\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\"><span key={notice.sequence}>{notice.text}</span></p>\n      {copyLayout ? <details ref={output} className=\"ot-tiling-output\"><summary>Layout JSON</summary><textarea ref={json} readOnly aria-label={`${label} layout JSON`} value={JSON.stringify(tiles, null, 2)} spellCheck={false} /></details> : null}\n    </div>\n  )\n}\n\nexport { Tiling, Tile, TilingEditor, type TilingProps, type TileProps, type TilingEditorProps, type TilingItem, type TilingLayout }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/tiling.css",
      "content": "@layer components {\n  /* ── ot-tiling ── tiles on the 12-column grid, held apart by space and one hairline, never boxed, after \"Less is more.\".\n     A tiling has no gaps and no overlaps, so neighbours share an edge: each tile draws its start and top edge halfway into the gap,\n     and the one line serves both sides. The edges that fall on the tiling's own border lie half a gap outside it and are clipped,\n     so no frame is drawn round the whole; the lines that stay run a step past the edge, as a draughtsman's construction lines do. */\n  .ot-tiling { --gap: var(--ot-space-6); --line: var(--ot-ink); --over: var(--ot-space-2); container-type: inline-size; min-width: 0; }\n  /* Keep the construction marks outside the sheet, but inside its container. */\n  .ot-tiling:not(.ot-tiling-editor) { padding-inline: calc(var(--gap) / 2); }\n  .ot-tiling[data-variant=\"crosses\"]:not(.ot-tiling-editor) { padding-inline: calc(var(--gap) / 2 + (var(--ot-space-3) + var(--ot-hairline)) / 2); }\n  .ot-tiling-sheet {\n    display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); grid-auto-rows: minmax(var(--ot-space-8), auto); gap: var(--gap);\n    clip-path: inset(calc(-1 * var(--over)));\n  }\n  .ot-tile { position: relative; display: flex; flex-direction: column; align-items: flex-start; min-width: 0; grid-column: span min(var(--span, 4), 12); grid-row: span var(--rows, 1); }\n  /* The top edge whole, the start edge under it, so no two lines ever cover one pixel twice. */\n  .ot-tile::before {\n    content: \"\"; position: absolute; inset: calc(var(--gap) / -2); pointer-events: none;\n    background:\n      linear-gradient(var(--line), var(--line)) 0 0 / 100% var(--ot-hairline) no-repeat,\n      linear-gradient(var(--line), var(--line)) 0 var(--ot-hairline) / var(--ot-hairline) calc(100% - var(--ot-hairline)) no-repeat;\n  }\n  .ot-tile:is([dir=\"rtl\"], [dir=\"rtl\"] *)::before { background-position: 0 0, 100% var(--ot-hairline); }\n  /* The corner the content keeps to. */\n  .ot-tile[data-place^=\"end\"] { align-items: flex-end; text-align: end; }\n  .ot-tile[data-place$=\"foot\"] { justify-content: flex-end; }\n  /* Narrow: twelve columns' gaps alone would outgrow the sheet, so it falls to two: a tile of up to half the grid takes one, a\n     wider one both; narrower still, one column, every tile the whole width and one row high. */\n  @container (width < 40rem) { .ot-tiling-sheet { grid-template-columns: repeat(2, minmax(0, 1fr)); } .ot-tile { grid-column: span clamp(1, (var(--span, 4) - 6) * 2, 2); } }\n  @container (width < 24rem) { .ot-tiling-sheet { grid-template-columns: minmax(0, 1fr); } .ot-tile { grid-column: auto; grid-row: auto; } }\n\n  /* crosses: after \"the uncreative\", which marks the corners of its block with + signs. No lines: the space alone holds the tiles\n     apart, and a registration cross stands at every corner of every tile, in the pencil, the outer ones a half gap outside the block. */\n  .ot-tiling[data-variant=\"crosses\"] > .ot-tiling-sheet { clip-path: none; }\n  .ot-tiling[data-variant=\"crosses\"] .ot-tile::before {\n    --x: calc(var(--ot-space-3) + var(--ot-hairline)); --h: calc(var(--x) / 2 - var(--ot-hairline) / 2); --c: var(--ot-pencil);\n    inset: calc(var(--gap) / -2 - var(--x) / 2);\n    background:\n      linear-gradient(var(--c), var(--c)) left 0 top var(--h) / var(--x) var(--ot-hairline),\n      linear-gradient(var(--c), var(--c)) left var(--h) top 0 / var(--ot-hairline) var(--x),\n      linear-gradient(var(--c), var(--c)) right 0 top var(--h) / var(--x) var(--ot-hairline),\n      linear-gradient(var(--c), var(--c)) right var(--h) top 0 / var(--ot-hairline) var(--x),\n      linear-gradient(var(--c), var(--c)) left 0 bottom var(--h) / var(--x) var(--ot-hairline),\n      linear-gradient(var(--c), var(--c)) left var(--h) bottom 0 / var(--ot-hairline) var(--x),\n      linear-gradient(var(--c), var(--c)) right 0 bottom var(--h) / var(--x) var(--ot-hairline),\n      linear-gradient(var(--c), var(--c)) right var(--h) bottom 0 / var(--ot-hairline) var(--x);\n    background-repeat: no-repeat;\n  }\n\n  /* Forced colours would drop the lines and crosses, which are background images: keep them, in CanvasText. */\n  @media (forced-colors: active) { .ot-tiling { --line: CanvasText; } .ot-tiling[data-variant=\"crosses\"] .ot-tile::before { --c: CanvasText; } .ot-tile::before { forced-color-adjust: none; } }\n\n  /* Editing gives each rectangle its own boundary. The static tiling above still shares rules.\n     All twelve columns fit the sheet, so touch can reach the corner without panning a tile. */\n  .ot-tiling-editor { --gap: clamp(var(--ot-space-1), 1cqi, var(--ot-space-3)); --ot-tiling-row: clamp(var(--ot-space-8), 16cqi, var(--ot-space-9)); inline-size: 100%; }\n  .ot-tiling-toolbar, .ot-tiling-actions { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--ot-space-2) var(--ot-space-5); }\n  .ot-tiling-toolbar { justify-content: space-between; }\n  .ot-tiling-caption { font-size: var(--ot-p); color: var(--ot-ink); }\n  .ot-tiling-editor button {\n    appearance: none; border: 0; background: none; color: var(--ot-graphite); padding: var(--ot-space-3) 0;\n    font: inherit; font-size: var(--ot-pp); line-height: var(--ot-pp-lh); min-block-size: var(--ot-space-6); cursor: pointer;\n  }\n  .ot-tiling-editor button:hover { color: var(--ot-ink); text-decoration: underline; text-underline-offset: var(--ot-space-1); }\n  .ot-tiling-editor button:disabled { color: var(--ot-pencil); cursor: default; text-decoration: none; }\n  .ot-tiling-hint, .ot-tiling-keys, .ot-tiling-status { font-size: var(--ot-pp); line-height: var(--ot-pp-lh); color: var(--ot-graphite); direction: ltr; unicode-bidi: isolate; }\n  .ot-tiling-instructions { margin-block: var(--ot-space-2) var(--ot-space-4); }\n  .ot-tiling-keys { visibility: hidden; margin-block-start: var(--ot-space-1); }\n  .ot-tiling-editor:has(.ot-tiling-viewport:focus-within) .ot-tiling-keys { visibility: visible; }\n  .ot-tiling-status { min-block-size: 3em; margin-block-start: var(--ot-space-3); }\n  .ot-tiling-viewport { overflow: auto; overscroll-behavior-inline: contain; padding: var(--ot-space-2); margin-inline: 0; }\n  .ot-tiling-viewport:focus-visible { outline-color: var(--ot-pencil); outline-offset: 0; }\n  .ot-tiling-canvas { min-inline-size: 0; }\n  .ot-tiling-columns { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: var(--gap); margin-block-end: var(--ot-space-3); color: var(--ot-pencil); font-size: var(--ot-pp); font-variant-numeric: tabular-nums; }\n  .ot-tiling-columns bdi { direction: ltr; unicode-bidi: isolate; }\n  .ot-tiling-editor .ot-tiling-sheet {\n    position: relative; isolation: isolate; grid-template-columns: repeat(12, minmax(0, 1fr));\n    grid-template-rows: repeat(var(--ot-tiling-rows, 2), var(--ot-tiling-row)); grid-auto-rows: var(--ot-tiling-row); clip-path: none;\n  }\n  .ot-tiling-editor .ot-tiling-sheet::before {\n    content: \"\"; position: absolute; inset: 0; z-index: -1; pointer-events: none; opacity: 0.45;\n    background-image: linear-gradient(to right, var(--ot-rule) var(--ot-hairline), transparent var(--ot-hairline)), linear-gradient(to bottom, var(--ot-rule) var(--ot-hairline), transparent var(--ot-hairline));\n    background-size: calc((100% + var(--gap)) / 12) calc(var(--ot-tiling-row) + var(--gap));\n  }\n  .ot-tiling-editor[data-variant=\"crosses\"] .ot-tiling-sheet::before {\n    background-image: radial-gradient(circle at var(--ot-hairline) var(--ot-hairline), var(--ot-rule-strong) 0 var(--ot-hairline), transparent calc(var(--ot-hairline) + 0.5px));\n  }\n  .ot-tiling-editor .ot-tile {\n    container-type: inline-size; min-block-size: 0; padding: var(--ot-space-4); cursor: grab; touch-action: none;\n    user-select: none; -webkit-user-select: none;\n  }\n  .ot-tiling-editor[data-variant] .ot-tile::before { inset: 0; background: none; border: var(--ot-hairline) solid var(--ot-rule-strong); }\n  .ot-tiling-editor .ot-tile:is(:hover, [data-selected], [data-held])::before { border-color: var(--ot-ink); }\n  .ot-tiling-editor .ot-tile[data-held] { z-index: 2; cursor: grabbing; }\n  .ot-tiling-editor .ot-tile:focus-visible { outline: none; }\n  .ot-tiling-editor .ot-tile:focus-visible::before { border-color: var(--ot-accent); }\n  .ot-tiling-editor .ot-tile[data-held]:focus-visible::before { border-color: var(--ot-ink); }\n  .ot-tiling-editor .ot-tiling-move { position: absolute; inset: 0; inline-size: 100%; block-size: 100%; padding: 0; cursor: inherit; touch-action: none; }\n  .ot-tiling-editor .ot-tiling-move:focus-visible { outline-offset: calc(-1 * var(--ot-space-1)); }\n  .ot-tiling-landing { position: relative; pointer-events: none; outline: var(--ot-hairline) dotted var(--ot-accent); outline-offset: var(--ot-space-1); }\n  .ot-tiling-editor .ot-tiling-size {\n    position: absolute; z-index: 3; padding: 0; inline-size: var(--ot-space-6); block-size: var(--ot-space-6); min-block-size: 0;\n    touch-action: none; user-select: none; -webkit-user-select: none; text-decoration: none;\n  }\n  .ot-tiling-size::after { content: \"\"; position: absolute; pointer-events: none; border-color: var(--ot-pencil); border-style: solid; border-width: 0; transition: inline-size var(--ot-allegro) var(--ot-exhale), block-size var(--ot-allegro) var(--ot-exhale); }\n  .ot-tiling-size[data-side=\"corner\"] { inset-block-end: 0; inset-inline-end: 0; cursor: nwse-resize; }\n  .ot-tiling-size[data-side=\"corner\"]:is([dir=\"rtl\"], [dir=\"rtl\"] *) { cursor: nesw-resize; }\n  .ot-tiling-size[data-side=\"corner\"]::after {\n    inset-block-end: var(--ot-space-2); inset-inline-end: var(--ot-space-2); inline-size: var(--ot-space-3); block-size: var(--ot-space-3);\n    border-inline-end-width: var(--ot-hairline); border-block-end-width: var(--ot-hairline);\n  }\n  .ot-tiling-size[data-side=\"corner\"]:is(:hover, :focus-visible)::after { inline-size: var(--ot-space-4); block-size: var(--ot-space-4); border-color: var(--ot-ink); }\n  .ot-tiling-size:not([data-side=\"corner\"]) { opacity: 0; }\n  .ot-tile:is(:hover, :focus-within, [data-selected]) .ot-tiling-size:not([data-side=\"corner\"]) { opacity: 1; }\n  .ot-tiling-size:is([data-side=\"start\"], [data-side=\"end\"]) { inset-block-start: calc(50% - var(--ot-space-6) / 2); cursor: ew-resize; }\n  .ot-tiling-size[data-side=\"start\"] { inset-inline-start: calc(-1 * var(--ot-space-2)); }\n  .ot-tiling-size[data-side=\"end\"] { inset-inline-end: calc(-1 * var(--ot-space-2)); }\n  .ot-tiling-size:is([data-side=\"start\"], [data-side=\"end\"])::after { inset-block-start: calc(50% - var(--ot-space-2)); block-size: var(--ot-space-4); border-inline-start-width: var(--ot-hairline); }\n  .ot-tiling-size[data-side=\"start\"]::after { inset-inline-start: var(--ot-space-2); }\n  .ot-tiling-size[data-side=\"end\"]::after { inset-inline-end: var(--ot-space-2); }\n  .ot-tiling-size:is([data-side=\"top\"], [data-side=\"bottom\"]) { inset-inline-start: calc(50% - var(--ot-space-6) / 2); cursor: ns-resize; }\n  .ot-tiling-size[data-side=\"top\"] { inset-block-start: calc(-1 * var(--ot-space-2)); }\n  .ot-tiling-size[data-side=\"bottom\"] { inset-block-end: calc(-1 * var(--ot-space-2)); }\n  .ot-tiling-size:is([data-side=\"top\"], [data-side=\"bottom\"])::after { inset-inline-start: calc(50% - var(--ot-space-2)); inline-size: var(--ot-space-4); border-block-start-width: var(--ot-hairline); }\n  .ot-tiling-size[data-side=\"top\"]::after { inset-block-start: var(--ot-space-2); }\n  .ot-tiling-size[data-side=\"bottom\"]::after { inset-block-end: var(--ot-space-2); }\n  /* A native hit surface makes the body a touch target too: Chromium must not\n     adjust a press in a short tile toward the nearby corner button. */\n  .ot-tiling-content { position: relative; inline-size: 100%; min-block-size: 0; overflow: auto; margin-block: auto; touch-action: none; pointer-events: none; }\n  .ot-tiling-content :is(a, button, input, textarea, select, [contenteditable], [data-tiling-no-drag]) { pointer-events: auto; }\n  .ot-tiling-content :is(input, textarea, select, [contenteditable], [data-tiling-no-drag]) { touch-action: auto; user-select: text; -webkit-user-select: text; }\n  .ot-tiling-name {\n    --ot-tiling-type-room: calc((var(--ot-tiling-row) * var(--rows) + var(--gap) * (var(--rows) - 1) - var(--ot-space-4) * 2) / var(--ot-mf-lh));\n    display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n    font: italic 400 min(calc(clamp(var(--ot-mp), 16cqi, var(--ot-ff)) * var(--ot-expression-scale)), var(--ot-tiling-type-room)) / var(--ot-mf-lh) var(--ot-expression); color: var(--ot-ink);\n  }\n  .ot-tiling-dimensions {\n    position: absolute; inset-block-start: var(--ot-space-2); inset-inline-start: var(--ot-space-3); display: flex; gap: var(--ot-space-1); align-items: center;\n    direction: ltr; unicode-bidi: isolate; font-size: var(--ot-pp); font-variant-numeric: tabular-nums; color: var(--ot-ink); pointer-events: none;\n  }\n  .ot-tiling-dimensions span:first-child { align-self: flex-start; font: italic 400 var(--ot-mp) / 1 var(--ot-expression); }\n  .ot-tiling-dimensions span:last-child { align-self: flex-end; }\n  .ot-tiling-empty { grid-column: 1 / 13; grid-row: 1 / 3; align-self: center; font-size: var(--ot-mf); line-height: var(--ot-mf-lh); color: var(--ot-ink); }\n  .ot-tiling-empty span { font-size: var(--ot-p); color: var(--ot-pencil); }\n  .ot-tiling-output { font-size: var(--ot-pp); color: var(--ot-graphite); }\n  .ot-tiling-output summary { cursor: pointer; padding-block: var(--ot-space-3); }\n  .ot-tiling-output textarea { display: block; inline-size: 100%; block-size: var(--ot-space-10); margin-block-start: var(--ot-space-3); padding: var(--ot-space-3); border: 0; border-block: var(--ot-hairline) solid var(--ot-rule); background: none; color: var(--ot-ink); font: inherit; resize: vertical; direction: ltr; unicode-bidi: isolate; }\n  @container (width < 24rem) { .ot-tiling-editor .ot-tile { padding-inline: var(--ot-space-2); } }\n  @media (forced-colors: active) {\n    .ot-tiling-editor { --ot-ink: CanvasText; --ot-rule-strong: CanvasText; --ot-accent: Highlight; }\n    .ot-tiling-editor .ot-tiling-sheet::before { forced-color-adjust: none; --ot-rule: GrayText; --ot-rule-strong: GrayText; }\n    .ot-tiling-editor .ot-tile::before { border-color: CanvasText; }\n    .ot-tiling-landing { outline-color: Highlight; }\n    .ot-tiling-size::after { border-color: ButtonText; }\n    .ot-tiling-editor .ot-tile:focus-visible::before { border-color: Highlight; }\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/tiling.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/tiling.css\"": {}
  },
  "meta": {
    "movement": "IV",
    "underneath": "native",
    "contract": "ot-tiling"
  },
  "type": "registry:ui"
}
