consumer documentation

Render a tiling layout in React — in one paste.

@n-uf/hypr-tiling is a controlled tiling renderer: you own a layout tree in state, and the component drags, drops, resizes, groups, and keyboard-controls its panes at runtime. Paste the block below and you have a working, resizable layout — the guides take it from there. These docs cover only the public API; contributors working on the library start from CONTRIBUTING.md.

import { useState, type ReactElement, type ReactNode } from "react";
import {
  TilingRenderer,
  DEFAULT_TILING_LAYOUT_CONFIG,
  type TilingLayoutNode,
  type TilingTile,
  type TilingRenderTileProps,
} from "@n-uf/hypr-tiling";

// The golden path: a controlled TilingRenderer. You own the layout tree in
// state; the renderer reports every user edit (drag, resize, group…) through
// onLayoutChange, and you apply it straight back. renderTile paints each pane.

const tiles: TilingTile[] = [
  { id: "editor", title: "Editor" },
  { id: "preview", title: "Preview" },
];

const initialLayout: TilingLayoutNode = {
  kind: "split",
  id: "root",
  axis: "horizontal",
  ratio: 0.5,
  first: { kind: "leaf", id: "left", tileId: "editor" },
  second: { kind: "leaf", id: "right", tileId: "preview" },
};

export function Quickstart(): ReactElement {
  const [layout, setLayout] = useState<TilingLayoutNode>(initialLayout);

  return (
    <TilingRenderer
      layout={layout}
      tiles={tiles}
      config={DEFAULT_TILING_LAYOUT_CONFIG}
      onLayoutChange={setLayout}
      renderTile={({ tile }: TilingRenderTileProps): ReactNode => (
        <div style={{ padding: 12, fontFamily: "system-ui", fontSize: 13 }}>
          {tile.title}
        </div>
      )}
    />
  );
}
live resultdrag · resize · group
Editor
Preview
golden path

Quickstart

Three steps to the layout above. Every step is runnable; there are no concept detours here — those come after, only as needed.

1

Install

Add the package and its React 19 peers.

pnpm add @n-uf/hypr-tiling react react-dom
2

Add the Tailwind content glob

The library ships utility classes in its dist, not CSS. Add that directory to your Tailwind content globs or the pane, divider, and drag-ghost classes get purged.

// tailwind.config.{js,ts}
export default {
  content: [
    "./src/**/*.{ts,tsx}",
    // Required: hypr-tiling ships utility classes in its dist output.
    // Without this glob Tailwind purges the pane / divider / ghost classes.
    "./node_modules/@n-uf/hypr-tiling/dist/**/*.{js,mjs,cjs}",
  ],
};
3

Render a controlled TilingRenderer

Own the layout tree in state, pass a config, and apply every edit the renderer reports through onLayoutChange. This is the exact source behind the live panes above — drag a header to rearrange, drag the divider to resize.

import { useState, type ReactElement, type ReactNode } from "react";
import {
  TilingRenderer,
  DEFAULT_TILING_LAYOUT_CONFIG,
  type TilingLayoutNode,
  type TilingTile,
  type TilingRenderTileProps,
} from "@n-uf/hypr-tiling";

// The golden path: a controlled TilingRenderer. You own the layout tree in
// state; the renderer reports every user edit (drag, resize, group…) through
// onLayoutChange, and you apply it straight back. renderTile paints each pane.

const tiles: TilingTile[] = [
  { id: "editor", title: "Editor" },
  { id: "preview", title: "Preview" },
];

const initialLayout: TilingLayoutNode = {
  kind: "split",
  id: "root",
  axis: "horizontal",
  ratio: 0.5,
  first: { kind: "leaf", id: "left", tileId: "editor" },
  second: { kind: "leaf", id: "right", tileId: "preview" },
};

export function Quickstart(): ReactElement {
  const [layout, setLayout] = useState<TilingLayoutNode>(initialLayout);

  return (
    <TilingRenderer
      layout={layout}
      tiles={tiles}
      config={DEFAULT_TILING_LAYOUT_CONFIG}
      onLayoutChange={setLayout}
      renderTile={({ tile }: TilingRenderTileProps): ReactNode => (
        <div style={{ padding: 12, fontFamily: "system-ui", fontSize: 13 }}>
          {tile.title}
        </div>
      )}
    />
  );
}

Next: paint your own pane content in Render your own content, then skim Concepts when you want the mental model.

recipes

How do I…

Each recipe is an outcome you want, a complete snippet that compiles against the current public API, the two or three knobs that matter, and where to look next.

Define the initial layout

Describe the starting arrangement as a plain, serialisable tree you own in state — a leaf holds one tile, a split divides space by a ratio, a group stacks leaves behind a tab strip.

import type { TilingLayoutNode, TilingTile } from "@n-uf/hypr-tiling";

// A layout is a plain, serialisable tree you own in state. Three node kinds:
//   • leaf  — holds one tile (by tileId)
//   • split — divides space along an axis (horizontal | vertical) by a ratio
//   • group — stacks several leaves behind a tab strip (one active at a time)
// Every node needs a stable `id`. A leaf's `tileId` points into your tiles.

export const tiles: TilingTile[] = [
  { id: "files", title: "Files" },
  { id: "editor", title: "Editor" },
  { id: "terminal", title: "Terminal" },
  { id: "problems", title: "Problems" },
];

// Files on the left; a stacked editor over a tabbed group (terminal + problems)
// on the right. This whole object is JSON-serialisable — persist and restore it.
export const initialLayout: TilingLayoutNode = {
  kind: "split",
  id: "root",
  axis: "horizontal",
  ratio: 0.25,
  first: { kind: "leaf", id: "files-pane", tileId: "files" },
  second: {
    kind: "split",
    id: "main",
    axis: "vertical",
    ratio: 0.7,
    first: { kind: "leaf", id: "editor-pane", tileId: "editor" },
    second: {
      kind: "group",
      id: "bottom-group",
      activeMemberId: "terminal-pane",
      members: [
        { kind: "leaf", id: "terminal-pane", tileId: "terminal" },
        { kind: "leaf", id: "problems-pane", tileId: "problems" },
      ],
    },
  },
};

Every node needs a stable id; a leaf’s tileId points into your tiles. Splits carry an axis ("horizontal" / "vertical") and a ratio in [0,1]; groups carry an activeMemberId. The whole object is JSON — persist and restore it verbatim.

Render your own content in a pane

Take over what each pane draws with a renderTile callback while the renderer keeps owning layout, resize, and drag.

import { useState, type ReactElement, type ReactNode } from "react";
import {
  TilingRenderer,
  DEFAULT_TILING_LAYOUT_CONFIG,
  type TilingLayoutNode,
  type TilingTile,
  type TilingRenderTileProps,
} from "@n-uf/hypr-tiling";

// renderTile paints the BODY and chrome of every pane — it's how you render your
// own content. The renderer still owns layout, splits, resize and drag; your job
// is to draw one pane and forward its interaction handles:
//   • root must be `article[data-leaf-id]` (the renderer resolves the drag source
//     from it) and wire onFocus / onPointerMove / onPointerLeave
//   • the header wires onHandlePointerDown (this is the drag-pickup handle)
// Everything else in TilingRenderTileProps (isFocused, isMaximized, the tile
// payload, …) is presentation state you style from.

const tiles: TilingTile[] = [
  { id: "chart", title: "Revenue", content: <strong>$1.2M</strong> },
  { id: "log", title: "Activity", content: <span>3 new events</span> },
];

const initialLayout: TilingLayoutNode = {
  kind: "split",
  id: "root",
  axis: "horizontal",
  ratio: 0.5,
  first: { kind: "leaf", id: "a", tileId: "chart" },
  second: { kind: "leaf", id: "b", tileId: "log" },
};

function Pane(args: TilingRenderTileProps): ReactElement {
  return (
    <article
      data-leaf-id={args.leafId}
      tabIndex={-1}
      onFocus={args.onFocus}
      onPointerMove={args.onPointerMove}
      onPointerLeave={args.onPointerLeave}
      style={{
        display: "flex",
        flexDirection: "column",
        height: "100%",
        border: args.isFocused ? "1px solid #fbbf24" : "1px solid #ffffff1a",
        borderRadius: 8,
        background: "#121316",
        overflow: "hidden",
      }}
    >
      <header
        onPointerDown={args.onHandlePointerDown}
        style={{ cursor: "grab", padding: "6px 10px", fontSize: 11, color: "#d6d3d1" }}
      >
        {args.tile.title}
      </header>
      <div style={{ padding: 10, fontSize: 13, color: "#e7e5e4" }}>
        {args.tile.content}
      </div>
    </article>
  );
}

export function RenderTileExample(): ReactElement {
  const [layout, setLayout] = useState<TilingLayoutNode>(initialLayout);
  return (
    <TilingRenderer
      layout={layout}
      tiles={tiles}
      config={DEFAULT_TILING_LAYOUT_CONFIG}
      onLayoutChange={setLayout}
      renderTile={(args: TilingRenderTileProps): ReactNode => <Pane {...args} />}
    />
  );
}
live resultdrag · resize · group
Revenue
$1.2M
Activity
3 new events

Root the pane on article[data-leaf-id] and forward onFocus, onPointerMove, and onPointerLeave; put onHandlePointerDown on your drag handle (the header). Everything else on TilingRenderTileProps (isFocused, isMaximized, the tile payload) is presentation state you style from. renderTile is a full-pane render prop, not a content slot — to draw your own header, buttons, and frame, see Render your own pane frame & header.

Render your own pane frame & header (full custom look-and-feel)

Own the ENTIRE pane, not just its body: draw your own frame, header, and controls, and wire them to the renderer’s drag, maximize, focus, and grouping handlers. Same generic renderTile prop as above — it returns the whole pane and hands you every interaction handle and state flag — no extra host-only prop is involved.

import { useState, type CSSProperties, type ReactElement, type ReactNode } from "react";
import {
  TilingRenderer,
  DEFAULT_TILING_LAYOUT_CONFIG,
  useTilingTheme,
  isMultiSelectModifierActive,
  TilingPaneRoot,
  TilingDragHandle,
  TilingPaneAction,
  TilingPaneBody,
  type TilingLayoutNode,
  type TilingTile,
  type TilingRenderTileProps,
  type TilingTheme,
} from "@n-uf/hypr-tiling";

// renderTile is a FULL-PANE render prop, not a content slot: it returns the
// WHOLE pane — frame, header, body — and receives every interaction handle +
// state flag the renderer computes. That means you can own the ENTIRE pane
// look-and-feel (your own frame, header, buttons, focus/drag affordances), not
// just `tile.content`, while the renderer keeps running layout, resize, drag,
// grouping, focus and keyboard control underneath. Grouping gestures to wire:
// a title-bar group icon (onToggleMultiSelect / onGroupMultiSelection /
// onClearMultiSelection / group.ungroup), Alt/Opt+click, and drag a header
// onto a title bar (groupDropTargetRef). Alt+G stays on the library keymap.
//
// There are two ways to write a custom pane, shown side by side below:
//
//   • THE EASY PATH — the optional helper primitives (TilingPaneRoot,
//     TilingDragHandle, TilingPaneAction, TilingPaneBody) encode the wiring
//     rules for you, so you only bring styling + content.
//   • THE RAW PATH — plain DOM wired by hand, the full escape hatch, for when
//     you need control the primitives don't give.
//
// The contract every custom pane must honor (the primitives encode all of it):
//   • root is `article[data-leaf-id={leafId}]` — the renderer resolves the drag
//     source from it (`closest("article[data-leaf-id]")`); wire onFocus,
//     onPointerMove and onPointerLeave on it too.
//   • your drag handle (typically the header) wires onHandlePointerDown with
//     `touch-action: none`.
//   • header action buttons stopPropagation on pointerDown + click so they don't
//     start a drag or steal focus.
//   • render the body only when paneBodyRenderMode === "render-content" (the
//     drag ghost reuses the same render path, so this keeps the ghost body in
//     sync and never empty).

const tiles: TilingTile[] = [
  { id: "editor", title: "editor", accent: "sky", content: <code>main.tsx</code> },
  { id: "preview", title: "preview", accent: "emerald", content: <span>rendered output</span> },
];

const initialLayout: TilingLayoutNode = {
  kind: "split",
  id: "root",
  axis: "horizontal",
  ratio: 0.5,
  first: { kind: "leaf", id: "a", tileId: "editor" },
  second: { kind: "leaf", id: "b", tileId: "preview" },
};

// THE EASY PATH — the same custom pane, built with the helper primitives. Each
// primitive owns one wiring rule; you supply only className/style and content.
function PrimitivesPane(args: TilingRenderTileProps): ReactElement {
  const theme: TilingTheme = useTilingTheme();
  return (
    <TilingPaneRoot
      pane={args}
      className={args.isFocused ? theme.resolveFocusFrame(args.tile.accent) : ""}
      style={{
        display: "flex",
        flexDirection: "column",
        height: "100%",
        borderRadius: 10,
        background: "#101114",
        overflow: "hidden",
        boxShadow: "0 18px 40px -30px rgba(0,0,0,0.9)",
      }}
    >
      <TilingDragHandle
        pane={args}
        style={{
          display: "flex",
          alignItems: "center",
          justifyContent: "space-between",
          gap: 8,
          padding: "7px 10px",
          cursor: "grab",
          borderBottom: "1px solid #ffffff14",
        }}
      >
        <span
          className={theme.resolveAccentText(args.tile.accent)}
          style={{ fontFamily: "ui-monospace, monospace", fontSize: 11, textTransform: "uppercase", letterSpacing: "0.14em" }}
        >
          {args.tile.title}
        </span>
        <span style={{ display: "flex", gap: 6 }}>
          {args.isMultiSelected && args.canGroupMultiSelection ? (
            <TilingPaneAction
              onClick={(): void => args.onGroupMultiSelection(args.leafId)}
              style={{ fontSize: 10, padding: "2px 6px", borderRadius: 6, border: "1px solid #fbbf2470", color: "#fde68a", background: "transparent" }}
            >
              group
            </TilingPaneAction>
          ) : null}
          {args.isMaximizeEnabled ? (
            <TilingPaneAction
              onClick={(): void => args.onToggleMaximize()}
              aria-label={args.isMaximized ? "restore pane" : "maximize pane"}
              style={{ fontSize: 12, lineHeight: 1, padding: "2px 6px", borderRadius: 6, border: "1px solid #ffffff1f", color: "#d6d3d1", background: "transparent" }}
            >
              {args.isMaximized ? "\u2715" : "\u2922"}
            </TilingPaneAction>
          ) : null}
        </span>
      </TilingDragHandle>
      <TilingPaneBody pane={args} style={{ flex: 1, minHeight: 0, overflow: "auto", padding: 12, fontSize: 13, color: "#e7e5e4" }}>
        {args.tile.content}
      </TilingPaneBody>
    </TilingPaneRoot>
  );
}

// THE RAW PATH — the identical pane wired by hand: article[data-leaf-id],
// header onPointerDown, buttons that stopPropagation, body render-mode gate.
// Only the public TilingRenderTileProps handles + flags and theme tokens are
// used — no debug/observability fields exist on the contract.
function RawPane(args: TilingRenderTileProps): ReactElement {
  const theme: TilingTheme = useTilingTheme();
  const frameStyle: CSSProperties = {
    display: "flex",
    flexDirection: "column",
    height: "100%",
    borderRadius: 10,
    background: "#101114",
    overflow: "hidden",
    outline: "none",
    boxShadow: "0 18px 40px -30px rgba(0,0,0,0.9)",
  };
  return (
    <article
      data-leaf-id={args.leafId}
      tabIndex={-1}
      onFocus={args.onFocus}
      onPointerMove={args.onPointerMove}
      onPointerLeave={args.onPointerLeave}
      style={frameStyle}
      // Focus frame comes from the active theme + this pane's accent — a custom
      // frame still reads as part of the themed surface.
      className={args.isFocused ? theme.resolveFocusFrame(args.tile.accent) : ""}
    >
      <header
        onPointerDown={args.onHandlePointerDown}
        onClick={(event): void => {
          // Alt/Opt+click toggles this pane's multi-selection without focusing.
          if (args.isMultiSelectGroupingEnabled && isMultiSelectModifierActive(event)) {
            event.stopPropagation();
            event.preventDefault();
            args.onToggleMultiSelect();
          }
        }}
        style={{
          display: "flex",
          alignItems: "center",
          justifyContent: "space-between",
          gap: 8,
          padding: "7px 10px",
          cursor: "grab",
          touchAction: "none",
          borderBottom: "1px solid #ffffff14",
        }}
      >
        <span
          className={theme.resolveAccentText(args.tile.accent)}
          style={{ fontFamily: "ui-monospace, monospace", fontSize: 11, textTransform: "uppercase", letterSpacing: "0.14em" }}
        >
          {args.tile.title}
        </span>
        <span style={{ display: "flex", gap: 6 }}>
          {args.isMultiSelected && args.canGroupMultiSelection ? (
            <button
              type="button"
              onPointerDown={(e): void => e.stopPropagation()}
              onClick={(e): void => {
                e.stopPropagation();
                args.onGroupMultiSelection(args.leafId);
              }}
              style={{ fontSize: 10, padding: "2px 6px", borderRadius: 6, border: "1px solid #fbbf2470", color: "#fde68a", background: "transparent" }}
            >
              group
            </button>
          ) : null}
          {args.isMaximizeEnabled ? (
            <button
              type="button"
              onPointerDown={(e): void => e.stopPropagation()}
              onClick={(e): void => {
                e.stopPropagation();
                args.onToggleMaximize();
              }}
              aria-label={args.isMaximized ? "restore pane" : "maximize pane"}
              style={{ fontSize: 12, lineHeight: 1, padding: "2px 6px", borderRadius: 6, border: "1px solid #ffffff1f", color: "#d6d3d1", background: "transparent" }}
            >
              {args.isMaximized ? "\u2715" : "\u2922"}
            </button>
          ) : null}
        </span>
      </header>
      <div style={{ flex: 1, minHeight: 0, overflow: "auto", padding: 12, fontSize: 13, color: "#e7e5e4" }}>
        {args.paneBodyRenderMode === "render-content" ? args.tile.content : null}
      </div>
    </article>
  );
}

export function CustomChromeExample(): ReactElement {
  const [layout, setLayout] = useState<TilingLayoutNode>(initialLayout);
  return (
    <TilingRenderer
      layout={layout}
      tiles={tiles}
      config={DEFAULT_TILING_LAYOUT_CONFIG}
      themeId="mosaic"
      onLayoutChange={setLayout}
      // The left pane uses the helper primitives (easy path); the right pane is
      // wired by hand (raw path). Both are fully interactive — drag, resize,
      // focus, maximize, group.
      renderTile={(args: TilingRenderTileProps): ReactNode =>
        args.leafId === "a" ? <PrimitivesPane {...args} /> : <RawPane {...args} />
      }
    />
  );
}
live resultdrag · resize · group
editor
main.tsx
preview
rendered output

Easiest path: the optional helper primitives. TilingPaneRoot spreads data-leaf-id + the focus/hover handlers, TilingDragHandle wires the drag pickup with touch-action: none and the Alt/Opt+click group toggle, TilingPaneAction is a header button that stops propagation so it never starts a drag or steals focus, and TilingPaneBody renders its children only in "render-content" mode. They encode the wiring rules so a custom pane can’t get them wrong; the raw renderTile args remain the full escape hatch. By hand: root on article[data-leaf-id] (the renderer resolves the drag source from it) and forward onFocus, onPointerMove, onPointerLeave. Put onHandlePointerDown on your header, expose onToggleMaximize, and add grouping via a title-bar group icon, Alt/Opt+click (onToggleMultiSelect + onGroupMultiSelection, gate the click with isMultiSelectModifierActive), or drag a header onto a title bar (groupDropTargetRef). Style from the state flags (isFocused, isMaximized, isDragSource) and compose with theme tokens from useTilingTheme() (resolveAccentText, resolveFocusFrame). Render the body only when paneBodyRenderMode is "render-content" so the drag ghost mirrors your pane. The ghost / cursor / cancel overlays mount on the overlay portal container (default document.body, see overlayPortalContainer) so position: fixed stays window-relative and the ghost stays outside the React root’s event-delegation scope. The homepage’s own tiles are built exactly this way — and the “panes” switch on the homepage flips them to a light minimalist variant built with these primitives.

Theme & color panes

Restyle the whole surface with a built-in theme, or give an individual pane its own accent — switching is live, with no remount.

import { useState, type ReactElement, type ReactNode } from "react";
import {
  TilingRenderer,
  TilingThemeProvider,
  useTilingTheme,
  resolveTilingTheme,
  DEFAULT_TILING_LAYOUT_CONFIG,
  type TilingLayoutNode,
  type TilingTile,
  type TilingThemeId,
  type TilingRenderTileProps,
} from "@n-uf/hypr-tiling";

// Two ways to theme:
//   1. Pick a built-in theme with the `themeId` prop ("neon-terminal" |
//      "clean-flat" | "mosaic"). Switching is live — no remount.
//   2. Give a pane its own per-tile accent via `tile.accent` (one of eight hues).
// Inside a pane you can read the active theme with useTilingTheme() — wrap a
// subtree in TilingThemeProvider to supply it (resolveTilingTheme maps an id to
// the token object).

const tiles: TilingTile[] = [
  { id: "a", title: "Signals", accent: "emerald" },
  { id: "b", title: "Alerts", accent: "rose" },
];

const initialLayout: TilingLayoutNode = {
  kind: "split",
  id: "root",
  axis: "horizontal",
  ratio: 0.5,
  first: { kind: "leaf", id: "l", tileId: "a" },
  second: { kind: "leaf", id: "r", tileId: "b" },
};

// A pane body that reads the live theme id from context.
function ThemedBody({ title }: { title: string }): ReactElement {
  const theme = useTilingTheme();
  return (
    <div style={{ padding: 12, fontSize: 13 }}>
      {title} · theme: {theme.id}
    </div>
  );
}

export function ThemingExample(): ReactElement {
  const [layout, setLayout] = useState<TilingLayoutNode>(initialLayout);
  const [themeId, setThemeId] = useState<TilingThemeId>("neon-terminal");

  return (
    <TilingThemeProvider theme={resolveTilingTheme(themeId)}>
      <button type="button" onClick={(): void => setThemeId("clean-flat")}>
        Use clean-flat
      </button>
      <TilingRenderer
        layout={layout}
        tiles={tiles}
        config={DEFAULT_TILING_LAYOUT_CONFIG}
        themeId={themeId}
        onLayoutChange={setLayout}
        onThemeChange={setThemeId}
        renderTile={({ tile }: TilingRenderTileProps): ReactNode => (
          <ThemedBody title={tile.title} />
        )}
      />
    </TilingThemeProvider>
  );
}
live resultdrag · resize · group
HYPR TILING
Signals · theme: neon-terminal
Alerts · theme: neon-terminal

Set themeId ("neon-terminal" / "clean-flat" / "mosaic") for the whole renderer; set tile.accent for one pane. useTilingTheme() reads the active theme inside a pane; resolveTilingTheme maps an id to its token object for TilingThemeProvider. Theme class tokens stay literal strings (Tailwind JIT) — that is not a portal constraint. Host CSS variables, data-theme, or a scoped dark class on a wrapper must pass that wrapper as overlayPortalContainer (element or thunk; default document.body) so the ghost inherits them. The container must not sit under a transform / filter / backdrop-filter / contain: paint ancestor; overlay z-indexes 220 / 230 are relative to that stacking context.

Choose which interactions are allowed

Turn interactions on or off — everything is enabled by default, so you subtract what you don’t want through one prop.

import { useState, type ReactElement, type ReactNode } from "react";
import {
  TilingRenderer,
  TILING_DASHBOARD_PRESET,
  resolveInteractionCapabilities,
  DEFAULT_TILING_LAYOUT_CONFIG,
  type TilingLayoutNode,
  type TilingTile,
  type TilingInteractionCapabilities,
  type TilingRenderTileProps,
} from "@n-uf/hypr-tiling";

// Every interaction (drag, resize, keyboard, grouping, maximize) is ON by
// default. Narrow behavior through the single `interaction` prop: pass a partial
// TilingInteractionCapabilities, or start from a preset and override. Here we
// take TILING_DASHBOARD_PRESET but disable grouping and lock resizing to the
// horizontal axis. resolveInteractionCapabilities() materializes the fully
// resolved shape if you need to read effective values yourself.

const interaction: TilingInteractionCapabilities = {
  ...TILING_DASHBOARD_PRESET,
  grouping: false,
  resize: "horizontal",
};

// Effective, fully-defaulted capabilities — handy for driving your own UI.
const resolved = resolveInteractionCapabilities(interaction);
export const isGroupingEnabled: boolean = resolved.grouping.enable;

const tiles: TilingTile[] = [
  { id: "a", title: "Board" },
  { id: "b", title: "Detail" },
];

const initialLayout: TilingLayoutNode = {
  kind: "split",
  id: "root",
  axis: "horizontal",
  ratio: 0.6,
  first: { kind: "leaf", id: "l", tileId: "a" },
  second: { kind: "leaf", id: "r", tileId: "b" },
};

export function CapabilitiesExample(): ReactElement {
  const [layout, setLayout] = useState<TilingLayoutNode>(initialLayout);
  return (
    <TilingRenderer
      layout={layout}
      tiles={tiles}
      config={DEFAULT_TILING_LAYOUT_CONFIG}
      interaction={interaction}
      onLayoutChange={setLayout}
      renderTile={({ tile }: TilingRenderTileProps): ReactNode => (
        <div style={{ padding: 12, fontSize: 13 }}>{tile.title}</div>
      )}
    />
  );
}
live resultdrag · resize · group
Board
Detail

Pass a partial TilingInteractionCapabilities to the interaction prop, or spread a preset like TILING_DASHBOARD_PRESET and override. resolveInteractionCapabilities returns the fully-defaulted shape when you need to read effective values. Every interaction is on by default; the lone opt-in is the tab strip’s dev-only content toggle (paneSwitching.showContentToggle), off unless you ask for it.

Save & restore a layout

Persist the arrangement across reloads. Because the tree is plain JSON you own, this is just save on change and load on mount.

import { useState, type ReactElement, type ReactNode } from "react";
import {
  TilingRenderer,
  DEFAULT_TILING_LAYOUT_CONFIG,
  type TilingLayoutNode,
  type TilingTile,
  type TilingRenderTileProps,
} from "@n-uf/hypr-tiling";

// Because YOU own the tree and it's plain JSON, persistence is just save/load.
// onLayoutChange fires on every user edit — write it to storage there. On mount,
// read it back (falling back to a default). No library-specific serializer.

const STORAGE_KEY = "my-app.layout";

const tiles: TilingTile[] = [
  { id: "a", title: "Nav" },
  { id: "b", title: "Content" },
];

const defaultLayout: TilingLayoutNode = {
  kind: "split",
  id: "root",
  axis: "horizontal",
  ratio: 0.3,
  first: { kind: "leaf", id: "l", tileId: "a" },
  second: { kind: "leaf", id: "r", tileId: "b" },
};

function loadLayout(): TilingLayoutNode {
  const raw = typeof localStorage !== "undefined" ? localStorage.getItem(STORAGE_KEY) : null;
  if (raw == null) {
    return defaultLayout;
  }
  // In real code, validate the parsed shape before trusting it.
  return JSON.parse(raw) as TilingLayoutNode;
}

export function SaveRestoreExample(): ReactElement {
  const [layout, setLayout] = useState<TilingLayoutNode>(loadLayout);

  const persist = (next: TilingLayoutNode): void => {
    setLayout(next);
    localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
  };

  return (
    <TilingRenderer
      layout={layout}
      tiles={tiles}
      config={DEFAULT_TILING_LAYOUT_CONFIG}
      onLayoutChange={persist}
      renderTile={({ tile }: TilingRenderTileProps): ReactNode => (
        <div style={{ padding: 12, fontSize: 13 }}>{tile.title}</div>
      )}
    />
  );
}

Write the layout to storage inside onLayoutChange and read it back in your useState initializer with a default fallback. Validate the parsed shape before trusting it in production. No library-specific serializer is involved.

Related in the reference

Run multiple workspaces over one tile pool

Switch between several saved arrangements that share the same tiles — like a tiling window manager — without remounting panes that exist in both workspaces.

import { useState, type ReactElement, type ReactNode } from "react";
import {
  DEFAULT_TILING_LAYOUT_CONFIG,
  TilingRenderer,
  TilingWorkspaceSwipeScope,
  WORKSPACE_KEY_BINDINGS,
  useTilingWorkspaceSetController,
  useTilingWorkspaceTabs,
  useWorkspaceSwipe,
  workspaceSetOfLayout,
  type TilingLayoutNode,
  type TilingRenderTileProps,
  type TilingTile,
  type TilingWorkspace,
  type TilingWorkspaceSet,
  type TilingWorkspaceSetCommitReason,
  type TilingWorkspaceSwitchEvent,
  type UseTilingWorkspaceTabsResult,
} from "@n-uf/hypr-tiling";

// Several layout trees over one tile pool: the set controller owns the
// persisted document + in-flight tree overlays; the tab strip is the native
// drop target; swipe / keymap / spring-load ride the same TilingCommand path.

const tiles: TilingTile[] = [
  { id: "a", title: "Board" },
  { id: "b", title: "Detail" },
];

const seedLayout: TilingLayoutNode = {
  kind: "split",
  id: "root",
  axis: "horizontal",
  ratio: 0.55,
  first: { kind: "leaf", id: "l", tileId: "a" },
  second: { kind: "leaf", id: "r", tileId: "b" },
};

function WorkspaceTabStrip({
  tabs,
  onCreate,
}: {
  readonly tabs: UseTilingWorkspaceTabsResult;
  readonly onCreate: () => void;
}): ReactElement {
  const swipe = useWorkspaceSwipe();
  return (
    <div {...tabs.tablistProps} data-swipe-progress={String(swipe.progress)}>
      {tabs.tabs.map((tab) => (
        <button key={tab.workspace.id} {...tab.tabProps}>
          {tab.workspace.name}
        </button>
      ))}
      <button type="button" onClick={onCreate}>
        +
      </button>
    </div>
  );
}

export function WorkspacesExample(): ReactElement {
  const [persisted, setPersisted] = useState<TilingWorkspaceSet>(() =>
    workspaceSetOfLayout(seedLayout),
  );
  const ctl = useTilingWorkspaceSetController({
    value: persisted,
    onCommit: (
      next: TilingWorkspaceSet,
      _reason: TilingWorkspaceSetCommitReason,
    ): void => {
      setPersisted(next);
    },
    mintWorkspaceId: (): string => crypto.randomUUID(),
    nextWorkspaceName: (existing: ReadonlyArray<TilingWorkspace>): string =>
      `Workspace ${existing.length + 1}`,
  });
  const tabs = useTilingWorkspaceTabs({
    workspaces: ctl.set,
    onWorkspacesChange: ctl.onWorkspacesChange,
  });
  return (
    <TilingWorkspaceSwipeScope>
      <WorkspaceTabStrip
        tabs={tabs}
        onCreate={(): void => {
          ctl.create();
        }}
      />
      <TilingRenderer
        workspaces={ctl.set}
        onWorkspacesChange={ctl.onWorkspacesChange}
        tiles={tiles}
        config={DEFAULT_TILING_LAYOUT_CONFIG}
        paneIdentity="stable"
        interaction={{
          keyBindings: { bindings: [...WORKSPACE_KEY_BINDINGS] },
          workspaces: {
            followMovedLeaf: true,
            switch: {
              transition: "slide",
              wheelSwipe: { modifier: "meta" },
              touchSwipe: true,
            },
            springLoad: { dwellMs: 500 },
          },
        }}
        onWorkspaceSwitch={(_event: TilingWorkspaceSwitchEvent): void => {
          /* via: key | command | swipe | spring-load | reveal | tab-drop */
        }}
        onMoveLeaf={(): void => {
          /* do not switch from here — followMovedLeaf already switched */
        }}
        {...tabs.rendererProps}
        renderTile={({ tile }: TilingRenderTileProps): ReactNode => (
          <div style={{ padding: 12, fontSize: 13 }}>{tile.title}</div>
        )}
      />
    </TilingWorkspaceSwipeScope>
  );
}
live resultdrag · resize · group

Pass workspaces + onWorkspacesChange instead of layout + onLayoutChange. tiles is the whole pool (every tile any workspace seats). Pair with useTilingWorkspaceTabs for tablist ARIA, keyboard, and native tab drop targets (tabs.rendererProps). Spread WORKSPACE_KEY_BINDINGS into interaction.keyBindings.bindings for default chords; dispatch switch-workspace, cycle-workspace, move-leaf-to-workspace, and reveal-tile through a TilingCommandHandle ref. Opt into trackpad swipe, slide/fade transition, and spring-loaded tab drop via interaction.workspaces (switch.wheelSwipe (optional modifier: "meta"; sequence-start gating + whole-window horizontal lock apply regardless), switch.transition, springLoad). followMovedLeaf: true moves and switches in one onWorkspacesChange (onWorkspaceSwitch via: "tab-drop") — do not switch again from onMoveLeaf. For a persisted document use useTilingWorkspaceSetController (value / onCommit, debounced tree edits). Set-wide orphan handling: orphanTiles + onIntegrityIssues.

Trigger actions from your own buttons

Drive the layout from a toolbar, menu, or any control you build — split, group, resize, maximize — without touching the tree by hand.

import { useRef, useState, type ReactElement, type ReactNode } from "react";
import {
  TilingRenderer,
  DEFAULT_TILING_LAYOUT_CONFIG,
  type TilingLayoutNode,
  type TilingTile,
  type TilingCommandHandle,
  type TilingRenderTileProps,
} from "@n-uf/hypr-tiling";

// Drive the layout from YOUR OWN buttons. Take the renderer's imperative handle
// with a ref, then dispatch typed TilingCommands — the same command set the
// keyboard and drag layers use. Great for toolbars, context menus, or a
// "reset layout" button. A command targeting a disabled capability is a safe
// no-op, so you never have to guard the happy path.

const tiles: TilingTile[] = [
  { id: "a", title: "Left" },
  { id: "b", title: "Right" },
];

const initialLayout: TilingLayoutNode = {
  kind: "split",
  id: "root",
  axis: "horizontal",
  ratio: 0.5,
  first: { kind: "leaf", id: "left", tileId: "a" },
  second: { kind: "leaf", id: "right", tileId: "b" },
};

export function CommandsExample(): ReactElement {
  const [layout, setLayout] = useState<TilingLayoutNode>(initialLayout);
  const handle = useRef<TilingCommandHandle>(null);

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 8, height: "100%" }}>
      <div style={{ display: "flex", gap: 6 }}>
        <button
          type="button"
          onClick={(): void =>
            handle.current?.dispatch({ kind: "set-split-ratio", splitId: "root", ratio: 0.7 })
          }
        >
          Widen left
        </button>
        <button
          type="button"
          onClick={(): void => handle.current?.dispatch({ kind: "toggle-split-axis", splitId: "root" })}
        >
          Flip axis
        </button>
        <button
          type="button"
          onClick={(): void =>
            handle.current?.dispatch({ kind: "group-leaves", leafIds: ["left", "right"] })
          }
        >
          Group both
        </button>
      </div>
      <div style={{ flex: 1, minHeight: 0 }}>
        <TilingRenderer
          ref={handle}
          layout={layout}
          tiles={tiles}
          config={DEFAULT_TILING_LAYOUT_CONFIG}
          onLayoutChange={setLayout}
          renderTile={({ tile }: TilingRenderTileProps): ReactNode => (
            <div style={{ padding: 12, fontSize: 13 }}>{tile.title}</div>
          )}
        />
      </div>
    </div>
  );
}
live resultdrag · resize · group
Left
Right

Take the renderer’s TilingCommandHandle with a ref and call dispatch with a typed TilingCommand (set-split-ratio, toggle-split-axis, group-leaves, …). A command targeting a disabled capability is a safe no-op, so you never have to guard the happy path.

Related in the reference

Build a command bar / keyboard shortcuts

advancedWire your own command bar or key bindings that only surface — and only fire — when the target command would actually do something.

import { useRef, useState, type KeyboardEvent, type ReactElement, type ReactNode } from "react";
import {
  TilingRenderer,
  resolveInteractionCapabilities,
  isCommandEnabled,
  DEFAULT_TILING_LAYOUT_CONFIG,
  type TilingCommand,
  type TilingCommandGates,
  type TilingCommandHandle,
  type TilingInteractionCapabilities,
  type TilingLayoutNode,
  type TilingTile,
  type TilingRenderTileProps,
} from "@n-uf/hypr-tiling";

// ADVANCED: build your own command bar / keyboard shortcuts. isCommandEnabled is
// the gate the renderer uses for its own shortcut chips — pass it a TilingCommand
// plus the gates derived from your resolved capabilities and it tells you whether
// the command would do anything. Use it to hide dead buttons and to keep a
// keyboard binding browser-graceful (only preventDefault when the command runs).

// Map resolved capabilities → the gate flags isCommandEnabled reads.
function gatesFor(interaction?: TilingInteractionCapabilities): TilingCommandGates {
  const caps = resolveInteractionCapabilities(interaction);
  return {
    maximizeEnabled: caps.maximize.enable,
    paneSwitchingEnabled: caps.paneSwitching.enable,
    focusEnabled: caps.focus,
    rearrangeEnabled: caps.rearrange,
    sizingEnabled: caps.paneTitleBarControls.sizing,
    acquireSpaceEnabled: caps.paneTitleBarControls.acquireSpace,
    collapseEnabled: caps.paneTitleBarControls.collapse,
    resizeEnabled: caps.resize !== "none",
    layoutEnabled: caps.masterLayout,
    groupingEnabled: caps.grouping.enable,
    workspacesEnabled: caps.workspaces.enable,
  };
}

interface BarEntry {
  readonly label: string;
  readonly key: string;
  readonly command: TilingCommand;
}

const ENTRIES: ReadonlyArray<BarEntry> = [
  { label: "Next pane", key: "]", command: { kind: "focus-cycle", direction: "next" } },
  { label: "Maximize", key: "m", command: { kind: "toggle-maximize" } },
  { label: "Master layout", key: "l", command: { kind: "cycle-layout-mode" } },
];

const tiles: TilingTile[] = [
  { id: "a", title: "One" },
  { id: "b", title: "Two" },
];

const initialLayout: TilingLayoutNode = {
  kind: "split",
  id: "root",
  axis: "horizontal",
  ratio: 0.5,
  first: { kind: "leaf", id: "l", tileId: "a" },
  second: { kind: "leaf", id: "r", tileId: "b" },
};

export function CommandBarExample(): ReactElement {
  const [layout, setLayout] = useState<TilingLayoutNode>(initialLayout);
  const handle = useRef<TilingCommandHandle>(null);
  const gates: TilingCommandGates = gatesFor();

  // Only surface commands that are actually enabled right now.
  const available: ReadonlyArray<BarEntry> = ENTRIES.filter((entry): boolean =>
    isCommandEnabled(entry.command, gates),
  );

  const onKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {
    const match = available.find((entry): boolean => entry.key === event.key);
    if (match != null) {
      event.preventDefault();
      handle.current?.dispatch(match.command);
    }
  };

  return (
    <div
      onKeyDown={onKeyDown}
      tabIndex={0}
      style={{ display: "flex", flexDirection: "column", gap: 8, height: "100%", outline: "none" }}
    >
      <div style={{ display: "flex", gap: 6 }}>
        {available.map((entry): ReactElement => (
          <button
            key={entry.label}
            type="button"
            title={`shortcut: ${entry.key}`}
            onClick={(): void => handle.current?.dispatch(entry.command)}
          >
            {entry.label}
          </button>
        ))}
      </div>
      <div style={{ flex: 1, minHeight: 0 }}>
        <TilingRenderer
          ref={handle}
          layout={layout}
          tiles={tiles}
          config={DEFAULT_TILING_LAYOUT_CONFIG}
          onLayoutChange={setLayout}
          renderTile={({ tile }: TilingRenderTileProps): ReactNode => (
            <div style={{ padding: 12, fontSize: 13 }}>{tile.title}</div>
          )}
        />
      </div>
    </div>
  );
}
live resultdrag · resize · group
One
Two

Derive TilingCommandGates from resolveInteractionCapabilities, then call isCommandEnabled(command, gates) to hide dead controls and keep a keyboard binding browser-graceful (only preventDefault when the command runs). This is the same gate the renderer uses for its built-in shortcut chips.

Group, split & maximize panes

Fold panes into a tabbed group, split space, or maximize a pane — as built-in gestures and from your own code.

import { useRef, useState, type ReactElement, type ReactNode } from "react";
import {
  TilingRenderer,
  DEFAULT_TILING_LAYOUT_CONFIG,
  type TilingLayoutNode,
  type TilingTile,
  type TilingCommandHandle,
  type TilingRenderTileProps,
} from "@n-uf/hypr-tiling";

// Grouping, splitting and maximizing are built-in interactions (Alt/Opt+G to
// group a selection, Alt+Enter to maximize, drag a header to split), but you can
// also drive them from code with commands:
//   • group-leaves     — fold leaves into one tabbed group
//   • insert-adjacent   — split: drop one leaf beside another
//   • toggle-maximize   — maximize / restore a leaf

const tiles: TilingTile[] = [
  { id: "a", title: "A" },
  { id: "b", title: "B" },
  { id: "c", title: "C" },
];

const initialLayout: TilingLayoutNode = {
  kind: "split",
  id: "root",
  axis: "horizontal",
  ratio: 0.34,
  first: { kind: "leaf", id: "a", tileId: "a" },
  second: {
    kind: "split",
    id: "rest",
    axis: "horizontal",
    ratio: 0.5,
    first: { kind: "leaf", id: "b", tileId: "b" },
    second: { kind: "leaf", id: "c", tileId: "c" },
  },
};

export function GroupSplitMaximizeExample(): ReactElement {
  const [layout, setLayout] = useState<TilingLayoutNode>(initialLayout);
  const [maximizedLeafId, setMaximizedLeafId] = useState<string | null>(null);
  const handle = useRef<TilingCommandHandle>(null);

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 8, height: "100%" }}>
      <div style={{ display: "flex", gap: 6 }}>
        <button
          type="button"
          onClick={(): void =>
            handle.current?.dispatch({ kind: "group-leaves", leafIds: ["b", "c"] })
          }
        >
          Group B + C
        </button>
        <button
          type="button"
          onClick={(): void => handle.current?.dispatch({ kind: "toggle-maximize", leafId: "a" })}
        >
          Maximize A
        </button>
      </div>
      <div style={{ flex: 1, minHeight: 0 }}>
        <TilingRenderer
          ref={handle}
          layout={layout}
          tiles={tiles}
          config={DEFAULT_TILING_LAYOUT_CONFIG}
          onLayoutChange={setLayout}
          maximizedLeafId={maximizedLeafId}
          onMaximizedLeafChange={setMaximizedLeafId}
          renderTile={({ tile }: TilingRenderTileProps): ReactNode => (
            <div style={{ padding: 12, fontSize: 13 }}>{tile.title}</div>
          )}
        />
      </div>
    </div>
  );
}
live resultdrag · resize · group
A
B
C

Users can group with the title-bar group icon, Alt/Opt+click, Alt/Opt+G, or by dragging a header onto a title bar; drag a header to split, and Alt+Enter to maximize out of the box. From code, dispatch group-leaves, insert-adjacent, and toggle-maximize. For custom multi-select affordances, isMultiSelectModifierActive reports whether the platform modifier is held.

mental model

Concepts

Three ideas unblock everything above. That is deliberately all — there is no architecture here.

The layout is a tree you ownA layout is a recursive tree of three node kinds: leaf (one tile), split (two children divided by a ratio along an axis), and group (leaves stacked behind a tab strip). It is plain, serialisable data held in your state.
The renderer runs interactions; you own the treeTilingRenderer is controlled. It performs drag, resize, grouping, focus, and keyboard control, then reports the resulting tree through onLayoutChange — it never mutates state behind your back. You apply the edit (or persist / diff / veto it).
Interactions are capabilities, on by defaultEvery interaction is enabled unless you narrow it. The single interaction prop (TilingInteractionCapabilities) subtracts or reshapes behavior; presets are just pre-filled partials. You configure by turning things off, not wiring things on — the one opt-in is the dev-only pane-content toggle, off by default.
copy wholesale

Examples gallery

Complete, controlled TilingRenderer apps. Each file is runnable as-is and type-checked against the public API — copy one and start editing. Neither passes renderTile, so each uses the renderer’s default pane chrome and is fully interactive: drag a header, drag a divider, Alt/Opt-click headers then group.

Metrics dashboard

A master-stack of accented metric panes on the clean-flat theme.

live resultdrag · resize · group
Revenue
MRR
$48.2k
Active users
DAU
12,904
Error rate
5xx / min
0.03%
Latency
p95
142 ms
import { useState, type ReactElement } from "react";
import {
  TilingRenderer,
  DEFAULT_TILING_LAYOUT_CONFIG,
  type TilingLayoutNode,
  type TilingTile,
} from "@n-uf/hypr-tiling";

// A whole runnable dashboard: four metric/detail panes in a master-stack shape,
// each with its own accent and content. Copy this file wholesale — it's a
// complete, controlled TilingRenderer app.
//
// No `renderTile` is passed, so the renderer supplies its DEFAULT pane chrome —
// a themed frame + header that is fully interactive out of the box: drag a header
// to rearrange, drag a divider to resize, Alt/Opt+click headers then "Group" to
// tab them, and maximize. Each pane paints its `tile.content` in the body (the
// dev-only content toggle is off by the library default, so content shows at
// rest). Reach for a `renderTile` callback only when you want to own the pane
// frame yourself — see the "custom look-and-feel" guide.

interface Metric {
  readonly label: string;
  readonly value: string;
}

function MetricCard({ metric }: { metric: Metric }): ReactElement {
  return (
    <div style={{ padding: 14, fontFamily: "system-ui" }}>
      <div style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: 1, color: "#a8a29e" }}>
        {metric.label}
      </div>
      <div style={{ fontSize: 26, fontWeight: 600, color: "#fafaf9" }}>{metric.value}</div>
    </div>
  );
}

const tiles: TilingTile[] = [
  { id: "revenue", title: "Revenue", accent: "emerald", content: <MetricCard metric={{ label: "MRR", value: "$48.2k" }} /> },
  { id: "users", title: "Active users", accent: "sky", content: <MetricCard metric={{ label: "DAU", value: "12,904" }} /> },
  { id: "errors", title: "Error rate", accent: "rose", content: <MetricCard metric={{ label: "5xx / min", value: "0.03%" }} /> },
  { id: "latency", title: "Latency", accent: "amber", content: <MetricCard metric={{ label: "p95", value: "142 ms" }} /> },
];

const initialLayout: TilingLayoutNode = {
  kind: "split",
  id: "root",
  axis: "horizontal",
  ratio: 0.5,
  first: {
    kind: "split",
    id: "left",
    axis: "vertical",
    ratio: 0.5,
    first: { kind: "leaf", id: "revenue-pane", tileId: "revenue" },
    second: { kind: "leaf", id: "users-pane", tileId: "users" },
  },
  second: {
    kind: "split",
    id: "right",
    axis: "vertical",
    ratio: 0.5,
    first: { kind: "leaf", id: "errors-pane", tileId: "errors" },
    second: { kind: "leaf", id: "latency-pane", tileId: "latency" },
  },
};

export function DashboardApp(): ReactElement {
  const [layout, setLayout] = useState<TilingLayoutNode>(initialLayout);
  return (
    <TilingRenderer
      layout={layout}
      tiles={tiles}
      config={DEFAULT_TILING_LAYOUT_CONFIG}
      themeId="clean-flat"
      onLayoutChange={setLayout}
    />
  );
}

Terminal grid

Monospace shell / logs / htop panes on the neon-terminal theme — the Hyprland homage, in the terminal.

live resultdrag · resize · group
zsh
$ pnpm dev
▸ ready in 312ms
$ _
logs
[info] listening :3000
[info] compiled ok
htop
cpu  12%
mem  3.1G / 16G
load 0.42
import { useState, type ReactElement } from "react";
import {
  TilingRenderer,
  DEFAULT_TILING_LAYOUT_CONFIG,
  type TilingLayoutNode,
  type TilingTile,
} from "@n-uf/hypr-tiling";

// A whole runnable "terminal grid" — the Hyprland homage made literal: several
// monospace shell panes a user splits, stacks and rearranges at runtime. Copy
// this file wholesale. No `renderTile` is passed, so the renderer's default pane
// chrome (fully interactive: drag / resize / group / maximize) frames each
// terminal and paints its `tile.content` body at rest.

function Terminal({ lines }: { lines: ReadonlyArray<string> }): ReactElement {
  return (
    <pre
      style={{
        margin: 0,
        padding: 12,
        height: "100%",
        overflow: "auto",
        fontFamily: "ui-monospace, monospace",
        fontSize: 12,
        lineHeight: 1.6,
        color: "#a7f3d0",
        background: "#0a0b0d",
      }}
    >
      {lines.join("\n")}
    </pre>
  );
}

const tiles: TilingTile[] = [
  { id: "shell", title: "zsh", content: <Terminal lines={["$ pnpm dev", "▸ ready in 312ms", "$ _"]} /> },
  { id: "logs", title: "logs", content: <Terminal lines={["[info] listening :3000", "[info] compiled ok"]} /> },
  { id: "top", title: "htop", content: <Terminal lines={["cpu  12%", "mem  3.1G / 16G", "load 0.42"]} /> },
];

const initialLayout: TilingLayoutNode = {
  kind: "split",
  id: "root",
  axis: "vertical",
  ratio: 0.6,
  first: {
    kind: "split",
    id: "top-row",
    axis: "horizontal",
    ratio: 0.5,
    first: { kind: "leaf", id: "shell-pane", tileId: "shell" },
    second: { kind: "leaf", id: "logs-pane", tileId: "logs" },
  },
  second: { kind: "leaf", id: "top-pane", tileId: "top" },
};

export function TerminalGridApp(): ReactElement {
  const [layout, setLayout] = useState<TilingLayoutNode>(initialLayout);
  return (
    <TilingRenderer
      layout={layout}
      tiles={tiles}
      config={DEFAULT_TILING_LAYOUT_CONFIG}
      themeId="neon-terminal"
      onLayoutChange={setLayout}
    />
  );
}
releases

Changelog

Newest first, including Unreleased. Calendar versioning (YY.M.R) cannot signal a SemVer major — breaking changes are flagged in the notes below, not by the version number. This page renders packages/hypr-tiling/CHANGELOG.md directly.

Unreleased

Nothing pending.

26.9.10

2026-09-24

Tab strips — rails and pills

Tabs are content-width pills in a rounded rail (2px pad / gap, 1px railBorderColor, radius + 2). They never stretch; they shrink only on overflow (min 56px, ellipsis, title tooltip). The strip row keeps a hairline bottom border and the strip background. Active chrome is a pill fill, not the old inset 0 2px 0 accent top edge — accent is the :focus-visible outline. Group-strip defaults use a quiet pill (tabActiveBackground: rgba(255,255,255,0.08)); pane-strip defaults (TILING_PANE_TAB_STRIP_THEME_DEFAULTS) use a solid pill (rgb(252, 211, 77) / rgb(12, 13, 16)). Additive tokens: railBackground, railBorderColor, nestedTabActiveBackground, nestedTabActiveColor. When a group member is maximized under a visible pane strip, the group row is the nested tier (data-nested="true", --hpt-tab-strip-nested, height − 4, ~18px indent, └ tick, nested active tokens).

Maximize

  • maximize.keepGroupTabStrip (default true) keeps the group tab strip when a group member is maximized: the group is painted at viewport size with a render-time activeMemberId override (layout is not mutated). This is a behaviour change for grouped leaves — strip height is subtracted from the maximized member. Set false for the previous leaf-only maximize. The top-level pane strip's group tab now labels the maximized member, not the stored active member.

Pane tab strip

  • paneSwitching.showTabStrip accepts true / false / "maximized". "maximized" paints the top-level strip only while a pane is maximized.
  • paneSwitching.tabStrip (placement, height, theme, renderTabLabel) themes that strip with the group-strip token vocabulary (--hpt-group-tab-*, including tabHoverColor). Default placement "top", height and theme match the group strip. "bottom" mounts the strip after the viewport. Root class hpt-pane-tab-strip (plus shared hpt-tab-strip).
  • Lab chrome (wordmark / pickers / content toggle) renders only when showTabStrip === true and those lab hooks are present.

Tab groups — built-in strip contrast

  • Inactive group tabs no longer paint at opacity: 0.55. Label color is tabColor at full opacity; hover uses the new tabHoverColor token (--hpt-group-tab-hover-color). Default tabColor stays rgb(148, 163, 184) on rgba(0, 0, 0, 0.45).
  • TilingGroupTabStripTheme.tabHoverColor is additive; omitted values keep the default rgb(226, 232, 240).

Docs

  • Home: the hero slot is a tab group, hypr-tiling | Proof | Scenarios, with the hero active on first paint; the four original tiles keep their places and widths. Proof facts (suite and test counts, gzip sizes, dependencies, license, version) are generated from the package. Scenarios run through the public command dispatch and the workspace-set controller.

26.9.9

2026-09-24

Tab groups — built-in strip is the canonical rendering

showGroupTabStrip already defaults to true (no default flip). Hosts that pass false keep the strip hidden. New groupTabStrip options are additive.

  • groupTabStrip.placement "top" (default, above the pane header) or "bottom" (under the pane body). height default 28 (px). Both placements subtract that height from the member content box.
  • showEject (default true) ejects the active member (remove-from-group). showUngroup (default true) ungroups. Both sit at the right end of the strip.
  • theme (TilingGroupTabStripTheme) is optional CSS values — background, borderColor, tabColor, tabActiveColor, tabBackground, tabActiveBackground, accent, fontFamily, fontSize, letterSpacing, radius, gap, paddingX, controlColor, controlHoverColor — resolved over the dark/amber default and applied as inline styles plus --hpt-group-tab-* custom properties (same partial-token path as dragChrome).
  • renderTabLabel(member) overrides the tile-title label. member is { id, tileId, title, active }.
  • The strip is a role="tablist" of role="tab" buttons (aria-selected, Left/Right/Home/End). Labels ellipsize with a title tooltip. The active-indicator transition is disabled under prefers-reduced-motion.
  • A drop on a built-in tab inserts at that tab's index (TilingDropIntentState.memberInsertIndex). A drop past the last tab, and every host groupDropTargetRef hit, still appends.

Multi-select grouping

  • Pane args gain multiSelectionCount: number — how many leaves are multi-selected in the active workspace (0 when none). Every pane of that workspace reports the same count, including panes that are not themselves selected, so a host can show Cancel when only one tile is selected. Inactive retained panes and the drag-ghost / drag-cancel surfaces report 0.

Drop preview

  • TilingLeafPreviewMode gains "group-merge". While a drag hovers a group-merge target (built-in group tab strip, or a host groupDropTargetRef hit with fallbackReason: "host-group-drop-target"), the target pane's preview.mode is "group-merge" (role: "drop-target-result-shadow"). The drag source and every other pane keep preview: null for that intent. Live drag mode still suppresses the per-tile preview.

26.9.8

2026-09-24

Docs

  • README, CHANGELOG intro and the docs home state the calendar-versioning contract: active development, releases may break compatibility, pin exact versions.

Multi-select grouping

  • Pane args gain onClearMultiSelection() — clears the whole selection from host chrome (same path as Escape).

Tab groups — host drop target

Additive. Hosts that hide grouping.showGroupTabStrip and paint their own chips can still offer drag-to-group.

  • TilingRenderTileProps.groupDropTargetRef: React.RefCallback<HTMLElement | null> — attach it to the element (title bar, chip strip, or both; the same callback may be attached to several elements) that should accept "drop here to group with this pane". Inert on the drag-ghost and drag-cancel surfaces.
  • While grouping.enable is on, a pointer over a registered element resolves group-merge on the same commit path as the built-in strip (deriveCandidateTree / projected layout). A loose leaf becomes a new group {target, source} with the source active; an existing group appends the source and makes it active. One commit; pane identity is preserved under paneIdentity: "stable". Works with showGroupTabStrip either true or false.
  • Precedence: built-in group tab strip, then the host element (it wins over the centre swap and over any edge band the element covers), then uncovered edge bands (edge-insert), then the pane centre (swap). The drag source's own target, and a group that already contains the source, fall through to that body partition.
  • Inserting at a hovered chip index is not part of this change; a group target appends.

26.9.7

2026-09-24

Workspaces — reset to defaults

Additive. No default key bindings; hosts opt in.

  • resetWorkspaceLayout(set, defaults, workspaceId?) — replace one workspace's layout and name from the seed workspace of the same id (workspaceId omitted → set.activeId). Missing seed id → layout: null, name kept. activeId unchanged. Same-reference when nothing changes.
  • resetWorkspaceSet(set, defaults) — restore the seed set, keeping set.activeId when that id exists in the seed. Same-reference when already equal.
  • workspaceSetEquals(a, b) — structural equality over ids, names, activeId, and persisted layout trees.
  • Commands { kind: "reset-workspace"; workspaceId? } and { kind: "reset-workspaces" }, gated by workspacesEnabled and the renderer workspaceDefaults seed (no-op without the seed). Emit through onWorkspacesChange; never fire onWorkspaceSwitch.
  • TilingRenderer set-mode prop workspaceDefaults?: TilingWorkspaceSet.
  • Controller defaults option, reset("workspace" | "all", workspaceId?), and atDefaults (true when reset("all") would change nothing, whichever workspace is active).

Docs

  • Workspace navigation (26.9.3–26.9.6) documented across the package README, root README, agent records, and docs site: set ops, set controller, renderer set-mode, tab strip, commands/keymap, swipe gates, transitions, and switch via including "tab-drop". Hosts must not switch again from onMoveLeaf. Changelog restores the missing ## 26.9.4 heading so 26.9.5 and 26.9.4 stay distinct.

26.9.6

2026-09-24

Drag-source mount identity across a tab drop. Additive; no default changes.

  • Fixed: a dragged pane no longer remounts when its drag has no in-tree candidate. Under paneIdentity: "stable" the live-drag display tree lifts the source out whenever the pointer is over nothing or over an external target (a workspace tab); the pool used to drop the pane's entry there, so the release — in-tree, external claim, or a move into another workspace — remounted it and lost in-pane local state. The source now stays parked in the pool through dragging and settling (last render props, transient flags cleared, like inactiveWorkspaces: "keep-mounted" retention) and the release reseats the same mount.
  • Tab drop honours `interaction.workspaces.followMovedLeaf`. A plain (non-spring-loaded) release on a workspace tab with followMovedLeaf: true moves the leaf AND switches activeId in ONE onWorkspacesChange, the same edge move-leaf-to-workspace takes, and fires onWorkspaceSwitch({ via: "tab-drop" }) + focus memory for the moved leaf. Hosts that switched from onMoveLeaf should stop: a second commit paints the leaf seated only in an inactive workspace first, which under inactiveWorkspaces: "unmount" is itself a remount. followMovedLeaf: false (default) is unchanged — the set is reported moved, not switched.
  • `TilingWorkspaceSwitchVia` gains `"tab-drop"` (additive union member; exhaustive switches over the union need a new arm).

26.9.5

2026-09-23

Three desktop disambiguation gates on the workspace wheel-swipe FSM. Hosts on 26.9.4 semantics keep modifier: null (gate 1 off) and pick up gates 2–3, which only remove false-positive arms. Touch swipe is unchanged except that it ignores modifier.

  • breakingBREAKING (type): `WheelInputSample` and the swipe `WHEEL` event require `metaKey` / `altKey` / `shiftKey`. Custom WheelTouchInputPort implementations must stamp the DOM modifier flags (the built-in createDomWheelTouchPort does). ctrlKey is still the pinch-zoom reject.
  • `TilingWorkspaceSwipeConfig.modifier: "meta" | "alt" | "shift" | null` — default null (no modifier required). When set, a wheel run arms only while that key is held on the arming samples; once tracking, releasing it does not cancel or convert the gesture to scroll. ctrlKey still rejects regardless. Touch ignores modifier. overscroll-behavior-x: contain still applies; the port preventDefaults only while tracking, so a wheel without the modifier scrolls normally.
  • Behaviour change (tightening): sequence-start gating. A wheel run is the samples with gaps < wheelIdleMs. Only the start of a fresh run may arm: if the first sample(s) fail (vertical-dominant, modifier missing, chain can still scroll, …), the run is claimed by scroll (idle.runClaimedByScroll + lastWheelTs) and later samples of that run cannot arm — even if they become horizontal. After lockout expiry, a momentum tail that continues the committed run still cannot arm.
  • Behaviour change (tightening): whole-window horizontal lock. |dx| > 2·|dy| still applies per sample at arming; the same ratio must also hold on the accumulated armed window (|travelDy| ≤ |travelPx| / 2) until travel reaches thresholdPx. A gradually diagonal path is rejected and the run claimed by scroll. Once tracking, vertical drift is still tolerated.
  • `switch.wheelSwipe: { modifier: "meta" }` resolves into switch.swipe.modifier through the existing partial-config path (resolveWorkspaceSwipeConfig).

26.9.4

2026-09-23

Engine-owned workspace navigation on top of the 26.9.3 workspace set: tile-keyed set ops and revealTile, typed workspace commands and an opt-in keymap fragment, a debounced useTilingWorkspaceSetController for persisted sets, pool-aware set rendering with explicit orphan policy, trackpad/touch swipe and slide/fade switch transitions, and spring-loaded tab drop with drag REARM. The set-mode wrapper dispatches every gesture through TilingCommand and reports the next TilingWorkspaceSet on onWorkspacesChange. DashAI (starpay-app/packages/dashai) is the first consumer cut-over target (S4/S5 after publish). Three breaking changes — read the first three bullets.

  • breakingBREAKING (type): `TilingCommand` gains four kinds — exhaustive switches with a `never` guard fail typecheck: switch-workspace (workspaceId or 1-based index), cycle-workspace, move-leaf-to-workspace (workspaceId or neighbour direction; omitted leafId → focused leaf; follow defaults from interaction.workspaces.followMovedLeaf), reveal-tile.
  • breakingBREAKING (type): `TilingCommandGates` gains `workspacesEnabled`: false in single-layout mode and when interaction.workspaces.enable is false; those commands are then a no-op.
  • breakingBREAKING (behaviour): set-mode layout healing is per active tree, not set-wide force-seat. In set mode tiles is the whole pool, but coverage is checked per tree: the active workspace's layout is healed against the tiles *it* seats only. Pre-26.9.4 the wrapper reused the single-layout heal, so a pool tile shown only in another workspace was force-seated into the active tree on the next integrity settle and a seat for an unknown tile was pruned. Single-layout mode is unchanged. Hosts that relied on the implicit prune must opt into orphanTiles: "seat-in-active" (see pool renderer bullets).

Engine set ops

  • `TilingWorkspacePlacement` `region` — { kind: "region"; region: "start" | "end" } seats the leaf left of the reading-order first leaf / right of the last, via insertLeafInto's adjacent path. A group-member neighbour seats beside the group node, not inside it. A null destination becomes the bare leaf. TILING_DEFAULT_WORKSPACE_PLACEMENT stays root-second.
  • `moveTileToWorkspace(set, tileId, to, placement?, options?)` — tile-keyed moveLeafToWorkspace. A tile seated nowhere is seated only when options.leaf.id is given (the engine never invents leaf ids); otherwise the set is unchanged.
  • `MoveTileToWorkspaceOptions` — { leaf?: { id } } mint id for moveTileToWorkspace when the tile has no seat.
  • `showTileInWorkspace(set, tileId, to, placement?)` — tile-keyed showInWorkspace. Unchanged when the tile is unknown, already shown in to, or to is unknown.
  • `hideTileFromWorkspace(set, tileId, from)` — tile-keyed hide; returns { set, orphaned }. orphaned is true when the tile now has no seat anywhere.
  • `TilingHideTileResult` — { set, orphaned } return of hideTileFromWorkspace.
  • `removeTile(set, tileId)` — drop every seat of the tile. Unchanged when the tile is seated nowhere.
  • `revealTile(set, tileId, prefer?)` — pick prefer when it shows the tile, else the first workspace in tab order that does; switch activeId and activate a group member tab as needed. Returns null when the tile is seated nowhere; same set reference when changed is "none".
  • `TilingRevealTileResult` / `TilingRevealTileChanged` — { set, workspaceId, leafId, changed: "none" | "tab" | "workspace" | "both" } return of revealTile.

Workspace commands & keymap

  • `interaction.workspaces?: boolean | { enable?; followMovedLeaf?; switch?; springLoad? }` — default enable: true, followMovedLeaf: false. Resolved as ResolvedTilingWorkspacesCapability (includes switch and springLoad; see swipe / transition / spring-load sections).
  • `WORKSPACE_KEY_BINDINGS` — opt-in chord→command fragment (Alt+Arrow cycle, Alt+1..9 switch by index, Alt+Shift+Arrow move+follow). Not merged into TILING_KEYMAP_DEFAULTS; hosts spread it into interaction.keyBindings.bindings.
  • Set-mode wrapper dispatches the four workspace commands through the existing TilingCommandHandle / dispatchCommand path and reports the next set on onWorkspacesChange.
  • `reveal-tile` dispatches engine revealTile(set, tileId, activeId) (prefers the active workspace when it shows the tile), updates the wrapper's focus memory for the revealed leaf, and fires onWorkspaceSwitch({ via: "reveal" }) when activeId changed.
  • `onWorkspaceSwitch?: (event: { from; to; via: "tab" | "key" | "command" | "swipe" | "spring-load" | "reveal" }) => void` — fired beside onWorkspacesChange when activeId changes through the renderer. "tab" is reserved (not emitted yet); a host tab strip that calls switch-workspace is reported as "command".

Set controller

  • `useTilingWorkspaceSetController` — host-agnostic session over a persisted TilingWorkspaceSet: tree edits (divider, rearrange, tab activation, in-tree drop) coalesce for treeDebounceMs (default 400) and commit once with reason "tree"; create / rename / remove / switch commit immediately as "lifecycle"; moveTile as "move"; reveal as "reveal" when changed !== "none". Every mutation folds pending trees first so a lifecycle commit never drops an in-flight tree edit. readOnly keeps switches local and commits nothing. Pending trees flush synchronously on unmount. Replaces the localTrees / pendingRef / lastPersistedRef block a set host (DashAI dashboard-renderer.tsx) keeps by hand.
  • `TilingWorkspaceSetCommitReason` — "tree" | "lifecycle" | "move" | "reveal".
  • `foldPendingTrees` / `viewedWorkspaceSet` / `diffWorkspaceTreeLayouts` / `adoptIncomingWorkspaceTrees` / `classifyIncomingWorkspaceSet` — framework-free helpers the hook uses (also on ./engine).

Pool-aware set renderer

  • Set mode `tiles` is the whole pool — pass every tile any workspace seats; active-tree healing never force-seats a tile seated only elsewhere (see breaking bullet above).
  • `orphanTiles?: "seat-in-active" | "ignore" | "report"` (set mode, default "report") — a pool tile no workspace seats / a seat whose tile is not in the pool. "report" leaves the set untouched and calls onIntegrityIssues; "seat-in-active" runs repairWorkspaceSet (orphans seated TILING_DEFAULT_WORKSPACE_PLACEMENT in the active tree, unknown seats pruned) once per set identity and emits the repaired set on onWorkspacesChange, then reports what it repaired; "ignore" does neither.
  • `mintLeafId?: (tileId) => string` (set mode) — leaf id for a seat minted by "seat-in-active"; default ` leaf:${tileId} `.
  • `onIntegrityIssues?: (issues: ReadonlyArray<TilingWorkspaceSetIssue>) => void` (set mode) — fires when the issue fingerprint (kind|workspaceId|leafId|tileId rows) changes; an empty list fires once when the last issue clears. Not called under "ignore".
  • `inactiveWorkspaces?: "unmount" | "keep-mounted"` (set mode, default "unmount") — "keep-mounted" keeps a tile-keyed stable pane (paneIdentity: "stable") whose tile is seated only in an inactive workspace parked in the hidden pool (display: none) instead of unmounting it. Costs: retained DOM + React subtree; retained only after first paint; empty active workspace still unmounts everything; no-op under paneIdentity: "slot".
  • `TilingRenderTileProps.workspaceId: string` / `seatCount: number` — the workspace the pane is painted in and how many workspaces seat the tile. Single-layout mode reports TILING_MAIN_WORKSPACE_ID / 1. A retained pane keeps the workspace id of its last live paint.
  • `TilingOrphanTilePolicy` / `TilingInactiveWorkspacesMode` — the two prop unions (. and ./engine).

Swipe navigation

  • `interaction.workspaces.switch?: { wheelSwipe?: boolean | Partial<TilingWorkspaceSwipeConfig>; touchSwipe?: boolean; transition?: "none" | "slide" | "fade" }` — opt-in swipe (both default false) and switch transition (default "none"). Resolved as ResolvedTilingWorkspaceSwitchCapability (resolveWorkspaceSwitchCapability on ./engine). When swipe is enabled the set-mode root carries overscroll-behavior-x: contain and touch-action: pan-y for touch. A committed swipe dispatches cycle-workspace and fires onWorkspaceSwitch({ via: "swipe" }).
  • `useWorkspaceSwipe(): { progress, target, phase }` + `TilingWorkspaceSwipeScope` — headless read of the gesture (progress -1..1, negative = previous). Live when the hook and the set-mode renderer share a scope. Types TilingWorkspaceSwipeSnapshot, TilingWorkspaceSwipePhase, TilingWorkspaceSwipeTarget; TILING_WORKSPACE_SWIPE_DEFAULTS on ..
  • `workspaceSwipeReducer(state, event, config?)` (./engine) — pure swipe FSM idle → armed → tracking → settling → lockout → idle. Commits when |progress| ≥ commitFraction or peak velocity ≥ commitVelocityPxMs; settling(commit) carries cycle-workspace. Helpers resolveSwipeArming, swipeProgressOfTravel, shouldCommitSwipe, swipeCommitCommand, hasSwipeNeighbour, swipeTargetOfTravel, workspaceSwipeSnapshot, resolveWorkspaceSwipeConfig; constants TILING_WORKSPACE_SWIPE_INITIAL_STATE / _EMPTY_CONTEXT / _IDLE_SNAPSHOT.
  • Swipe settle waits for the switch transition — with switch.transition not "none", the swipe FSM stays settling until WorkspaceTransitionStage finishes (commit or cancel); SETTLE_DONE then moves to lockout / idle. With "none", reduced motion, or no mounted stage the settle is immediate. settleGate on UseWorkspaceSwipeDriverParams (wrapper implements it over the stage's onSettled).
  • Ports — WheelTouchInputPort / WheelTouchInputListener / WheelInputSample / TouchInputSample and ScrollChainPort (./engine); DOM adapters createDomWheelTouchPort, normaliseWheelDelta, createDomScrollChainPort, canElementScrollFurther (.).

Switch transition

  • `TilingWorkspaceTransitionMode` — "none" | "slide" | "fade". Default "none".
  • `TilingWorkspaceTransitionDirection` — "prev" | "next".
  • `TilingWorkspaceTransitionConfig` / `DEFAULT_WORKSPACE_TRANSITION_CONFIG` — { mode: "none", durationMs: 200, easing }.
  • `resolveTransitionMode(requested, { reducedMotion, degraded })` — reduced motion → "none"; degraded canvas capture falls "slide" → "fade".
  • `transitionTransform(progress, direction, mode)` — compositor-friendly translateX / opacity for outgoing clone and live incoming tree.
  • `captureViewClone(root)` — viewport cloneNode(true); copies <canvas> via drawImage; strips hit-test data-*; degraded: true on tainted / oversized canvases (16 M pixel budget).
  • `WorkspaceTransitionStage` / `useWorkspaceTransition` — stage + begin({ direction, mode }) / scrub(progress) / finish("commit" | "cancel"). The set-mode wrapper mounts the stage inside the viewport and drives it: begin before onWorkspacesChange for renderer-driven switches and before a host-driven activeId change; tracking swipe scrubs progress; short release settles with finish("cancel"); committed swipe keeps its capture. cycle-workspace keeps cycle direction across wrap-around; other switches derive prev / next from tab order. Hosts that manually drove useWorkspaceTransition should use interaction.workspaces.switch.transition instead.
  • `theme.workspaceTransition?: { durationMs; easing }` — optional motion tokens; resolveWorkspaceTransition fills 200 ms + drag-hop easing.

Spring-loaded tab drop

  • `springLoadReducer(state, event, config?)` (./engine) — dwell FSM idle → dwelling → fired over workspace-tab hover; TICK at dwellMs fires once with intent: { kind: "commit-and-rearm", leafId, workspaceId, placement }. Helpers springLoadFireAt, isWorkspaceTabDragHover; constants TILING_SPRING_LOAD_INITIAL_STATE; types TilingSpringLoadState / TilingSpringLoadEvent / TilingSpringLoadIntent / TilingSpringLoadPhase.
  • `TILING_SPRING_LOAD_DEFAULTS` / `TilingSpringLoadConfig` — { dwellMs: 500 }; resolveSpringLoadConfig(partial?). `TilingSpringLoadCapability` — Partial<TilingSpringLoadConfig> | false; resolveSpringLoadCapability(value) → config or null.
  • `interaction.workspaces.springLoad?: TilingSpringLoadCapability | null` (set mode, default off) — while a pane drag reports kind: "workspace-tab" external hover over a non-active workspace, the wrapper runs springLoadReducer and arms a dwell timer. On fire: claimed external commit, one onWorkspacesChange with moveLeafToWorkspace + switchWorkspace (with transition when configured), onMoveLeaf + onWorkspaceSwitch({ via: "spring-load" }), then drag REARM on the leaf's new seat under the held pointer. Active workspace tab never dwells; release / Escape mid-dwell runs ordinary tab drop / cancel.
  • Drag machine `REARM` event (DragRearmEvent, ./engine) — starts a drag from idle without pointerdown at the current pointer on a new leaf seat after spring-load commit. Accepted only in idle (canRearmDrag). rearmPointerAnchorOffset(footprint, client) — default grab offset.

26.9.3

2026-09-22

Engine-native workspace set (_agent/workspace-set-concept.md, library half H1–H4), plus the titlebar-only pane collapse that was pending as "Unreleased", plus the React-free ./engine entry fix. Additive minor for the workspace set; the collapse work carries two default-only breakages — read its first two bullets.

Note

26.10.0 was published under a mis-numbered calendar version (month 10 ≠ September) and was unpublished from npm; 26.9.3 supersedes it with identical workspace-set API plus the React-free ./engine entry. (YY.M.R: M is the calendar month of the release — September is 9.)

Workspace set

  • `TilingWorkspaceSet` — { workspaces: [{ id, name, layout }], activeId } over one tile pool. Pure set → set ops: createWorkspace, renameWorkspace, deleteWorkspace (refuses the last; returns removedTileIds shown only there), switchWorkspace, cycleWorkspace, setWorkspaceLayout, moveLeafToWorkspace(set, leafId, to, placement?), showInWorkspace, hideFromWorkspace, workspaceSetOfLayout, activeWorkspace, findWorkspaceById, queryWorkspaceSet, normalizeWorkspaceName. Limits TILING_WORKSPACES_MAX (12) and TILING_WORKSPACE_NAME_MAX_CHARS (40) are enforced at create / rename.
  • Integrity — workspaceSetIssues(set, { expectedTileIds? }) walks the invariants (unique non-empty workspace ids, valid names, activeId exists, every tree structurally valid, one leaf id ↔ one tile id across the set, no orphan / unknown tile against expectedTileIds); repairWorkspaceSet fixes deterministically and returns the reasons.
  • `TilingWorkspacePlacement` + insertLeafInto / extractLeafNode (engine) — the seat-and-extract halves the movers share, now public so the set ops reuse them instead of duplicating tree code.
  • `TilingRenderer` workspace-set mode — workspaces + onWorkspacesChange (TilingRendererWorkspaceSetProps) as the alternative to layout + onLayoutChange; TilingRendererProps is unchanged and the mode-independent surface is TilingRendererCommonProps. The active tree is painted; a switch does not remount a pane whose leaf exists in both workspaces. Uncontrolled focus and maximize are remembered per workspace; a drag in flight is cancelled when its source leaf leaves the controlled tree (workspace switch). renderEmptyWorkspace(workspace) fills a null layout. onMoveLeaf(leafId, from, to) reports a tab drop.
  • Native workspace-tab drop target — TilingExternalDragHover is now the union TilingExternalDropHover | TilingWorkspaceTabDragHover (a bare { targetId, point } literal is still the former). A kind: "workspace-tab" hover settles in set mode by moveLeafToWorkspace through the 26.9.2 claim-before-settle path — no host hit-test, no host claim. resolveWorkspaceTabHover(targets, point) is the pure hit-test; TilingGhostChipContext.workspaceId names the destination for the chip.
  • `resolveExternalDragHover` / `onExternalDragHoverChange` renderer props — engine-driven hover resolution (called per processed drag frame and synchronously at release) as the alternative to feeding externalDragHover from host state.
  • `onExternalDrop` now receives the full hover as a 4th argument and may return false to decline the claim (the release then cancels with the fly-back). Existing 3-argument void hosts are unaffected.
  • Headless `useTilingWorkspaceTabs` / `TilingWorkspaceTabs` — tablist / tab / tabpanel props, roving focus, Arrow / Home / End / Enter / Space / F2 / Delete (resolveWorkspaceTabKey is the pure model), onRenameRequest / onCloseRequest affordances with tab.rename / tab.close / create committing through the engine ops, and rendererProps that make every tab a native drop target. Renders nothing; classes come only from the new theme tokens.
  • Theme tokens TilingTheme.workspaceTabs, workspaceTab, workspaceTabActive, workspaceTabDropTarget (all optional strings).

Pane collapse (previously "Unreleased")

  • breakingBREAKING (default only): `resizeFloor` now defaults to `"chrome"`. An interactive resize can size a pane out to its collapsed titlebar/chrome extent with no opt-in. The old content floor remains as resizeFloor: "body" on TilingLeafNode (per-leaf) or TilingLayoutConfig (library-wide).
  • breakingBREAKING (default only): a collapsed pane's body stays mounted. TilingLayoutConfig.collapseBodyMode is "keep-mounted" | "unmount" (default "keep-mounted"). The prior unmount-on-collapse behavior is collapseBodyMode: "unmount".
  • Titlebar-only pane collapse (`paneTitleBarControls.collapse`, opt-in). Collapse pins the pane to its chrome extent on the split axis; the body is hidden (display: none). Collapsed panes stay draggable. Axis is recorded so a drag re-parent reasserts the pin on the new split. Both-collapsed siblings stay collapsed; leftover axis space is an intentional split slack void (drag or nudge the divider to expand both).
  • `onPaneCollapsedChange` — { leafId, collapsed } after the layout edit, so a host can retitle or react without parsing the tree.
  • `minBBoxPx` — leaf-scoped resize floor that travels with the pane.
  • `TilingCommandGates.collapseEnabled` gates toggle-collapse / set-collapsed. Non-interactive titleBarContent no longer swallows titlebar drag.

Fixed

  • `./engine` entry no longer pulls the React renderer chunk; safe to import from server / react-server layers (Next route handlers). In 26.10.0 (the mis-numbered first publish of this release) dist/engine.mjs re-exported from the code-split chunk that also backs . (the whole renderer — createContext, hooks, theme), and engine.ts itself re-exported accentHue, BASELINE_DRAG_HOP_DURATION_MS, and INSTANT_DRAG_DURATION_MS from react/. Importing @n-uf/hypr-tiling/engine from a Next.js route handler therefore evaluated React's react-server build and failed with TypeError: createContext is not a function. The three demoted symbols now live in the engine layer (engine/accent-hues.ts, engine/drag-timing.ts; same names, same values, . exports unchanged), and engine.ts is built as its own tsup configuration with code splitting off, so dist/engine.{mjs,cjs} is a standalone bundle with no react / react-dom import and no chunk shared with . / ./devtools. Guarded by __tests__/engine-entry-react-free.test.ts (pnpm test:dist, run in CI and prepublishOnly) and by the engine ↛ react layering guardrail, which now also scans the engine.ts entry file. No public API change (all three API reports identical).

26.9.2

2026-09-21

Compact drag ghost + external-drop claim (26.9.x standalone subset of the WorkspaceSet drag scope). Additive; default drag behaviour is unchanged.

  • `TilingRendererProps.dragGhostMode` — "footprint" (default, today's tile-sized ghost) | "compact" (always the cursor-anchored chip) | "auto" (chip while externalDragHover is set, footprint otherwise). Footprint ↔ compact transitions over ~140 ms scale/opacity; prefers-reduced-motion is instant. Same overlayPortalContainer as the footprint ghost.
  • `TilingRendererProps.externalDragHover` — host-reported { targetId, point } while the pointer is over chrome outside the tiling tree. The engine does not hit-test host chrome.
  • `TilingRendererProps.onExternalDrop` — (leafId, targetId, point) fired on release while hover is set, before the FSM settles. Marks the drag claimed and skips DragCancelOverlay. Absent → existing cancel + fly-back.
  • Theme slot `TilingTheme.ghostChip` — (ctx: { leafId, title?, point, targetId? }) => ReactNode. Host paints the chip contents; default is a minimal neutral chip (title / leaf id).

26.9.1

Patch release. The drag overlays (ghost, custom cursor, cancel fly-back) still default to a document.body portal so position: fixed stays window-relative, but a host can now redirect that portal:

  • `TilingRendererProps.overlayPortalContainer` — HTMLElement | null | (() => HTMLElement | null). The thunk is evaluated every render so a late-mounted container (ref / callback) is picked up without remounting the renderer. null / omitted falls back to document.body. Both DragPaneOverlay and DragCursorOverlay (and the cancel fly-back) use the same resolved container.
  • Containing-block caveat: the container must not sit under an ancestor with transform / filter / backdrop-filter / perspective / contain: paint (or layout / strict / content) / will-change of those. The overlays still use window-relative client coordinates; a containing-block ancestor reintroduces ghost↔seat drift.
  • Use this when host theme tokens (CSS variables on a scoped root) must inherit into the ghost. Do not add a second scoping mechanism. Overlay z-indexes (ghost 220, cursor 230, cancel 219) are relative to the container's stacking context, not the document.
  • The body default exists for two independent reasons: containing-block immunity (position: fixed stays window-relative) AND the ghost staying outside the React root's event-delegation scope. Redirecting the DOM mount does not change the delegation invariant — React still walks fibers, and measurement selectors stay root/viewport-scoped.

26.9.0

Feature release (calendar-aligned YY.M.R; 26.9 = September, .0 = first release of the month). One breaking theme-shape change — read the first bullet.

  • breakingBREAKING (theme shape): `paneShell.dragSourceOpacity` removed. The picked-up source pane's dim is now dragChrome.sourcePane (see below) and is applied by the RENDERER to the whole leaf wrapper — title AND content, one opacity — for the default tile and a custom renderTile alike. Custom panes that applied their own isDragSource fade should drop it (it now double-dims). A consumer theme object that still spells paneShell.dragSourceOpacity fails typecheck (excess property); delete the line or move the value to dragChrome: { sourcePane }.
  • New theme slot `dragChrome` (`TilingThemeDragChromeTokens`, optional + partial). Every renderer-painted drag surface reads theme tokens, so a host can make the drag state look exactly like its at-rest pane. Tokens: ghostLifted / ghostSeated / ghostTransition (ghost WRAPPER elevation / scale / opacity / tint delta — outside the ghost.surface shell), cancelFlyBack, sourceReservation (the content-less seat's full surface — radius / border / background / shadow), sourcePane (whole-pane source dim), dropTarget (drop-target leaf highlight, default none), dropIntentLayer (default tile's edge/center hint frame radius + border width), cursorBadge + cursorBadgeValid / cursorBadgeInvalid / cursorBadgeNeutral (the pointer-pinned drop-validity badge shape + tones), and resolveSeatFrame(accent) (the seat's hop-in frame; default = resolveFocusFrame). resolveDragChrome(theme) fills omitted tokens with a PANE-SHELL-INHERITING default: the seat wears paneShell.surface, the ghost wrapper carries a small neutral elevation + opacity-95, no drop-target highlight. A theme that passes nothing never shows a look it did not author.
  • Default drag look changed for `clean-flat` / `mosaic` / consumer themes. The renderer no longer hardcodes the neon look in the drag path: no rounded-xl seat, no scale-[1.01] + slate-950 drop shadow on the ghost wrapper, no cyan/rose/slate cursor badge unless the theme asks. The built-in neon-terminal theme now carries that exact look in its own dragChrome, so the showcase is unchanged.
  • Live-drag observability layers default OFF. TILING_OBSERVABILITY_COLOR_ENABLE_DEFAULTS.dragSourceBorderEnabled / dragTargetBorderEnabled are now false: the pink seat tint + source border and the cyan drop-target border / fill / inset shadow were inline-style debug overlays painting over every consumer's drag. Toggle them in the observability panel (or pass observabilityColorEnables) when diagnosing drop resolution. Preview-mode projected-landing layers are unchanged.
  • Stable pane identity across drag → drop → settle (`paneIdentity` prop, `TilingPaneIdentityMode = "auto" | "stable" | "slot"`). The split tree is reconciled positionally, so an insert drop (leaf moves to another branch) used to unmount + remount the pane and a swap handed each instance the other tile's props — host content re-initialized after every drop. In "stable" mode panes render ONCE each in a hidden tile-keyed pool and their DOM node is relocated (layout-effect appendChild) into whichever slot shows their tile; the React instance (hooks, state, refs, iframes, scroll) is the same object through drag → drop → settle and every other tree edit. "auto" (default) is "stable" on a client-only mount and "slot" (the legacy in-place render, SSR-faithful) when hydrating server markup; pass "stable" explicitly to opt an SSR host in (server HTML then carries empty slots; content is placed on the client). DOM shape in stable mode: the leaf wrapper carries data-hpt-pane-slot="<tileId>", the pane sits in a display: contents wrapper [data-hpt-pane], and a [data-hpt-pane-pool] (display: none) sits after the viewport inside the root. The drag ghost is still a transient second render of the tile through renderTile (the overlay portal container, default document.body, see overlayPortalContainer / 26.9.1) so position: fixed stays window-relative AND the ghost stays outside the React root's event-delegation scope — see _agent/drag-subsystem-audit.md §11.
  • Leaf wrapper drag attributes. data-drag-source-pane (preview-mode dimmed source) and data-drop-target-pane (resolved drop target) on the leaf wrapper for host CSS hooks; the ghost wrapper carries data-drag-ghost-wrapper.

26.7.2

2026-07-30
  • Layout reconciliation (`normalizeLayout`). Commit-time normalization on every resize/rearrange pointerup / pointercancel / lostpointercapture demotes unfit static pins, clamps ratios against min-pane + full gutters, and enforces the both-static filler invariant so panes+gutters fill the container. Live resize ratio updates are rAF-coalesced; an idle settle (~150ms) and a dev fill-slack invariant (measureLayoutFillSlackPx) are belt-and-suspenders. Exported from @n-uf/hypr-tiling/engine as normalizeLayout, measureLayoutFillSlackPx, LAYOUT_RECONCILE_IDLE_MS, LAYOUT_FILL_SLACK_TOLERANCE_PX.
  • Tile-slot integrity on reconcile. normalizeLayout accepts optional expectedTileIds / fallbackLayout. When the host declares expected tiles, reconcile detects duplicate/missing/unknown/empty tileIds and rebuilds a sane default dwindle (or the host fallback) instead of leaving void slots. Collapsed ratios near 0/1 are clamped in place. New helpers: assessLayoutTileIntegrity, buildDefaultDwindleLayout, LAYOUT_COLLAPSED_RATIO_EPS. isStructurallyValidLayout also rejects duplicate tileIds (at most one tile per slot).
  • Hard layout integrity (empty / overlap / gone). Reconcile now auto-derives expectedTileIds from the host tiles registry on every resize/rearrange commit and idle settle. Gap-closed rearrange candidates that drop a host tile are refused (cancel fly-back). Geometry assessment flags zero-area leaves, overlapping footprints, and fill-slack voids; unhealable cases rebuild. Clear API: assertLayoutIntegrity, repairLayout, expectedTileIdsFromHostTiles, layoutCoversExpectedTiles.
  • Pane titlebar middle slot. TilingTile.titleBarContent renders custom chrome between the title (left) and native window controls (right). The default tile wires the slot; custom renderTile panes can wrap tile.titleBarContent with the new TilingPaneTitleBarContent primitive (stops pointer-down so toolbar interactions do not start a rearrange drag). Drag ghosts carry the same slot. Existing title-only headers are unchanged.

26.7.1

A docs/tooling patch — no runtime or public-API changes. (Calendar-aligned versioning YY.M.R; the trailing .1 is the release counter within 26.7.)

  • npm README now uses absolute URLs. The README rendered on npmjs.org resolves asset and doc references against npm's own CDN, not the GitHub repo, so the previous repo-relative paths broke: the logo image 404'd and the etc/hypr-tiling.api.md API-report links went nowhere. Asset references now point at absolute raw.githubusercontent.com URLs and doc links at github.com/n-uf/hypr-tiling/blob/main/..., so the logo renders and the API-report links resolve on the npm package page.
  • Removed an invalid `ignoreDeprecations: "6.0"` value from the apps/web and packages/showcase tsconfigs. That value is not a legal ignoreDeprecations target under TypeScript 6.0.3 and was rejected by the compiler; dropping it lets those workspaces typecheck cleanly.

26.7.0

First published release of @n-uf/hypr-tiling, a dynamic tiling layout engine for React. This entry is the baseline: earlier working versions were never published, so the changelog tracks the package from this release forward.

Entry points

The package exposes three import paths through package.json#exports ("sideEffects": false):

  • `@n-uf/hypr-tiling` (.) — the public API. A small, hand-authored facade of explicit named exports (never export *); this is the ONLY consumer surface and the one tracked for compatibility.
  • `@n-uf/hypr-tiling/devtools` — opt-in observability overlays, on their own entry so a renderer-only consumer never bundles them.
  • `@n-uf/hypr-tiling/engine` — a @beta escape hatch that re-exports the engine-grade, framework-free internals (layout-tree reducers, low-level tree walkers, keymap and drag-adjacent math) for power users driving the tree headlessly. No stability guarantees — it may change or disappear in any release and is kept off the consumer documentation site.

Public API surface (`.`)

  • `TilingRenderer` — the layout renderer: controlled layout tree, focus and maximize, drag-and-drop with FLIP animation and self-healing recovery, multi-select grouping (Alt/Opt+G), pane switching, and per-tile accents. A custom renderTile receives the clean, debug-free TilingRenderTileProps contract (tile payload + pane state flags + interaction handlers) — the drag/drop observability + debug fields are OFF this surface (they route through the internal default pane and /devtools).
  • Custom-pane helper primitives — optional, unstyled conveniences layered over renderTile that encode the pane wiring rules so a custom pane can't get them wrong: TilingPaneRoot (data-leaf-id root + focus/hover handlers), TilingDragHandle (drag pickup + touch-action: none + Alt/Opt group toggle), TilingPaneAction (action button that stops propagation), and TilingPaneBody (renders children only in render-content mode). The raw renderTile args stay the full escape hatch.
  • Theming — TilingThemeProvider / TilingTheme for token-driven styling.
  • Layout inspection & mutation — the layout is a recursive tree of TilingLayoutNode (TilingLeafNode / TilingSplitNode / TilingGroupNode). Read it with queryTilingLayout (a TilingLayoutQuery view: leaf ids, splits, groups, tile order, directional-neighbor lookup). Mutate it declaratively via onLayoutChange or imperatively by dispatching a typed TilingCommand through the renderer's TilingCommandHandle (gated by isCommandEnabled). The raw pure reducers (groupLeaves, insertLeafAdjacent, updateSplitRatio, …) are NOT on the public entry — they live on @n-uf/hypr-tiling/engine.
  • Interaction & presets — TilingInteractionCapabilities, resolveInteractionCapabilities, TILING_DASHBOARD_PRESET, and the theming registry constants. Every interaction is on by default and narrowed by passing a partial interaction prop; the single opt-IN exception is the group tab strip's dev/demo "show pane body" checkbox (paneSwitching.showContentToggle, default false), so a consumer that renders its own pane content never surfaces an end-user control that blanks it and panes paint content at rest with no wiring.

Consumer theming & chrome contract

Every painted pixel is reachable from a consumer-authored TilingTheme or routed through the consumer renderTile — no renderer state paints chrome the consumer didn't choose. Defaults stay zero-config (built-in themes, DefaultTilingTile, the built-in group strip). Concretely:

  • `theme` prop on `TilingRendererProps` — a full consumer-authored TilingTheme; takes precedence over themeId. TilingTheme.id is open (TilingThemeId | (string & {})) so a consumer mints its own theme id; TILING_THEME_REGISTRY stays keyed by the closed built-in union, and the built-in theme switcher (onThemeChange, typed TilingThemeId) remains built-ins-only — consumers running a custom theme simply don't wire it.
  • `grouping` capability object form — boolean | TilingGroupingCapability ({ enable?, showGroupTabStrip? }; bare boolean = { enable }, showGroupTabStrip default true). showGroupTabStrip: false suppresses the built-in per-group tab strip so a consumer paints its own group chrome; keyboard group commands stay live. paneSwitching.showTabStrip governs the TOP-LEVEL tab strip only.
  • `surface` discriminator on `TilingRenderTileProps` (TilingRenderSurface: "pane" | "drag-ghost" | "drag-cancel"). The two drag surfaces carry the REAL resolved capability display flags (no mid-drag silhouette pop) with inert no-op handlers; a custom pane that wants different drag chrome branches on surface.
  • `group` context on `TilingRenderTileProps` — TilingRenderTileGroupContext | null, populated for a tabbed group's ACTIVE member: the TilingGroupMemberView member list (leaf/tile ids, resolved tile, 1-based memberNumber, isActive) plus activateMember / removeMember / ungroup callbacks that route through the same internal command router as the built-in strip and the keyboard layer. null for loose leaves and drag surfaces.
  • Cancel fly-back fidelity — the drag-cancel overlay renders through the consumer renderTile (surface: "drag-cancel") when one is supplied, so a custom skin keeps its own chrome for the whole cancel glide; the built-in shell remains the no-renderTile fallback.
  • Hand-authored facade — 104 public API items. Engine-grade internals are physically layered under engine/ and reached only through the . facade (via react/) or the explicit ./engine entry. An API Extractor report per entry is checked in (etc/hypr-tiling{,.devtools,.engine}.api.md); pnpm api:check fails CI if the . surface drifts or an unexported type leaks onto it, and an architectural guardrail keeps the engine/ layer framework-free and blocks deep consumer imports.

Developer / observability tooling — `@n-uf/hypr-tiling/devtools`

The observability panel, its seed defaults, and the whole debug/observability input surface live on a separate /devtools subpath, so a renderer-only consumer never pulls them into its bundle:

``ts import { TilingObservabilityPanel, ANIMATION_CONTROL_DEFAULTS, TilingRenderer, // the observability-instrumented view of the same renderer } from "@n-uf/hypr-tiling/devtools"; ``

The renderer's observability inputs — overlay colors (observabilityColors / observabilityColorEnables), the hit-zone / drop-intent debug flags, and the onDropIntentChange / onLiveHitLogChange / onProjectedOverlayCountChange telemetry hooks — are collected into TilingRendererObservabilityProps and kept OFF the consumer TilingRendererProps contract. /devtools exports both those props and the observability-typed view of the SAME TilingRenderer component that accepts them, plus the debug/observability snapshot types they reference (TilingDropIntentDebugState, TilingLiveHitLogState, TilingObservabilityColorConfig, TilingPaneHitZone*, …). The consumer . surface carries none of them.

Documentation

Guides and a generated API reference are published at hypr-tiling.n-uf.com/docs. Every public symbol carries TSDoc (coverage enforced in CI), so summaries and examples surface in editor hover-docs.

Tailwind requirement

The package ships no CSS — it emits Tailwind utility classes. Add @n-uf/hypr-tiling to your Tailwind content glob or the renderer is unstyled. See the README "Tailwind content requirement" section.

reference · for when you already know the name

API reference

The generated per-symbol reference for the curated public API surface, produced from the library’s source TSDoc via API Extractor and API Documenter. This is a fallback — reach for it once you already know a symbol name; the guides above are the way in. Internal and devtools-only symbols are excluded, so every entry is part of the supported consumer contract, grouped by category and browsable from the sidebar tree. The full machine-readable report lives in the API report.

reference · renderer & tiles

Renderer & tiles

The TilingRenderer component, its props, and the tile + custom render-prop contract.

TilingRenderer

variable

The controlled tiling renderer — the single component a consumer mounts. Accepts either a controlled layout (TilingRendererProps) or a controlled workspace set (TilingRendererWorkspaceSetProps); the two are discriminated by the presence of workspaces. The public prop surface is the curated, debug-free contract. The underlying component also accepts the devtools-tier TilingRendererObservabilityProps; that widened view is exported as TilingRenderer from @n-uf/hypr-tiling/devtools for the observability panel.

Signature:

TilingRenderer: React.ForwardRefExoticComponent<TilingRendererModeProps & React.RefAttributes<TilingCommandHandle>>

TilingRendererProps

interface

Props for the TilingRenderer component in SINGLE-LAYOUT mode — the full controlled-component surface. layout + tiles + config + onLayoutChange are the four required props; everything else is optional and resolves to a documented default. The mode-independent part is TilingRendererCommonProps; the workspace-set alternative is TilingRendererWorkspaceSetProps.

Signature:

interface TilingRendererProps extends TilingRendererCommonProps 

Extends: TilingRendererCommonProps

Remarks

The renderer is a CONTROLLED component: you hold the layout tree in state and apply every edit it reports through onLayoutChange. Focus, maximize, theme, and accent can each be left uncontrolled (renderer-managed) or lifted into your state via the matching on*Change callback. Interaction behavior is tuned entirely through the single interaction prop (TilingInteractionCapabilities). This surface is debug-free: the drag/drop observability overlays, hit-zone visualizations, and telemetry feeds are a devtools concern, exposed through TilingRendererObservabilityProps on the @n-uf/hypr-tiling/devtools entry rather than here.

Example

const [layout, setLayout] = useState<TilingLayoutNode>(initialLayout);
return (
  <TilingRenderer
    layout={layout}
    tiles={tiles}
    config={DEFAULT_TILING_LAYOUT_CONFIG}
    onLayoutChange={setLayout}
  />
);

Properties

Property

Modifiers

Type

Description

layout

TilingLayoutNode

The controlled layout tree to render. Apply every reported edit back here.

onLayoutChange

(layout: TilingLayoutNode) => void

Called with the next layout tree whenever the renderer edits it (controlled).

TilingRenderTileProps

interface

The argument object passed to a custom renderTile. This is the clean, consumer-facing pane contract: the tile payload, this pane's derived state (focus / drag / drop / sizing), and the imperative callbacks a custom pane surface wires to its header, drag handle, and controls. Every field is something a custom pane legitimately reads to style itself or wires to drive an interaction — nothing here is a debug, observability, or tooling concern (those live on the internal render path and the /devtools surface).

Signature:

interface TilingRenderTileProps 

Remarks

A minimal custom pane needs only three wiring rules, each of which the optional public helper primitives (TilingPaneRoot(), TilingDragHandle(), TilingPaneAction(), TilingPaneBody()) encode so they cannot be gotten wrong: - the pane root must carry data-leaf-id={leafId} (the renderer resolves the drag source through the [data-leaf-id] attribute) and wire onFocus, onPointerMove, onPointerLeave; - the drag handle wires onHandlePointerDown with touch-action: none; - header action buttons must stopPropagation on pointer-down + click so a click does not start a drag or steal focus; - the body renders only when paneBodyRenderMode === "render-content" (the drag ghost reuses the same render path).

Properties

Property

Modifiers

Type

Description

canGroupMultiSelection

boolean

Whether the current multi-selection (≥2 panes) can be folded into one flat group right now — i.e. group-leaves would change the layout. Any mix of loose panes and/or existing-group members flattens into ONE group (groups the selection touches are dissolved and their members folded in). Drives whether the Group control is offered; false hides it rather than offering a no-op (e.g. the selection is already exactly one group).

collapsedDimension

TilingDimension | null

Which dimension collapse pinned ("width" under a horizontal/ side-by-side parent, "height" under a vertical/stacked parent) — see TilingLeafNode.collapsedDimension. null while isCollapsed is false. A custom renderTile pane uses this (not a pixel-size guess) to pick axis-appropriate collapsed chrome, e.g. a narrow vertical titlebar for a "width" collapse vs. the short horizontal strip for "height".

dropZone

TilingLeafDropZone | null

The resolved drop zone under the cursor for this pane, or null.

group

readonly

TilingRenderTileGroupContext | null

Group context when this pane is a tabbed group's ACTIVE member (the only member that renders, per the stacking contract); null for loose leaves and on the "drag-ghost" / "drag-cancel" surfaces. See TilingRenderTileGroupContext.

groupDropTargetRef

readonly

React.RefCallback<HTMLElement | null>

Ref callback for the element that accepts a drag-to-group drop on this pane (a custom chip strip, the title bar, or both — the same callback may be attached to more than one element). While grouping.enable is on, a pointer over any of those elements during a drag resolves group-merge into this pane, on the same commit path as the built-in group tab strip: a loose leaf becomes a new group {this pane, source} with the source active; an existing group appends the source and makes it active.

The hit is resolved before pane-body zone classification, so it wins over the centre swap and over any edge band the element overlaps — the same way the built-in strip wins over the group body. Pixels the element does not cover keep the body partition (edge bands stay edge-insert, the centre stays swap). The built-in strip is checked first when both contain the point. Ineligible hits (this pane is the drag source, or the source is already a member of this pane's group) fall through to that body partition. Inert on the drag-ghost / drag-cancel surfaces.

Gated by grouping.enable. Works with showGroupTabStrip either way.

heightSizingMode

TilingPaneSizingMode

This pane's current HEIGHT sizing mode (static/flexible) — drives the active control state.

isCollapsed

boolean

Whether THIS pane is currently collapsed to titlebar-only (HT-PANE-COLLAPSE).

isCollapseEnabled

boolean

Whether the per-pane collapse control is enabled (paneTitleBarControls.collapse).

isDragSource

boolean

Whether this pane is the source of the in-flight drag.

isDropEligible

boolean

Whether this pane is a valid drop destination for the current drag.

isDropTarget

boolean

Whether this pane is the resolved drop target of the in-flight drag.

isFocused

boolean

Whether this pane currently holds focus.

isHoveringDropCandidate

boolean

Whether the cursor is currently hovering this pane as a drop candidate.

isInvalidDrop

boolean

Whether the current hover over this pane would be an invalid drop.

isMaximized

boolean

Whether this pane is currently maximized (fills the tiling viewport).

isMaximizeEnabled

boolean

Whether the maximize capability is enabled (controls header button visibility).

isMoveSource

boolean

Whether this pane is the SOURCE of an in-flight keyboard move (the keyboard analog of a drag source). Drives the "MOVING" affordance.

isMultiSelected

boolean

Whether THIS pane is currently a member of the multi-selection set.

isMultiSelectGroupingEnabled

boolean

Whether the Alt/Opt+click header multi-selection feature is live (paneSwitching.multiSelectGrouping AND the grouping capability). When false, the header click handler must treat a modified click as a plain click and never render the Group control.

isPaneContentVisible

boolean

Global pane-content visibility toggle from the tab strip checkbox. false hides pane body content by default.

isRearrangeEnabled

boolean

Whether drag-to-rearrange is enabled (drives handle affordance).

isTitleBarAcquireSpaceEnabled

boolean

Whether the per-pane title-bar directional acquire-space controls (→ ← ↑ ↓) are enabled.

isTitleBarSizingEnabled

boolean

Whether the per-pane title-bar sizing control (FLEX / STATIC H / STATIC W / BOTH) is enabled.

leafId

string

The leaf node id this render targets.

moveTargetPlacement

TilingMovePlacement | null

When this pane is the pending move-mode DESTINATION, the edge of this pane the moved source will land on (insertLeafAdjacent placement). null when this pane is not the current move-mode target.

multiSelectionCount

readonly

number

How many leaves are multi-selected in the active workspace. 0 when the set is empty. The same count is reported on every pane of that workspace, including panes that are not themselves selected, so a host can show a Cancel control when the count is 1 (the Group control stays hidden until canGroupMultiSelection is true). Inactive retained panes and the drag-ghost / drag-cancel surfaces report 0.

onAcquireSpace

(direction: TilingFocusDirection) => void

Grow THIS pane to claim the maximum available space in direction by pushing matching-axis ancestor dividers toward the limit (siblings clamped to their minimum). Emits via onLayoutChange (controlled).

onClearMultiSelection

readonly

() => void

Clear the whole multi-selection set (all panes). No-op when the set is empty or the feature is off. Wire to a host "Cancel" control.

onFocus

(event?: React.SyntheticEvent<HTMLElement>) => void

Establish single focus on this pane (and clear any in-progress multi-selection). Wire to the pane root's onFocus. The renderer reads the focus event's target: when a multi-selection is active and focus landed on a header CONTROL button (Group / maximize), it is a no-op, so a click on the Group button is not nullified by the button stealing focus first.

onGroupMultiSelection

(clickedLeafId: string) => void

Group the current multi-selection into ONE flat tabbed group occupying the CLICKED pane's slot (pass this pane's leafId), then clear the selection. The clicked pane becomes the host (active tab); any existing group in the selection is dissolved and folded in. Wire to the Group control.

onHandlePointerDown

(event: React.PointerEvent<HTMLElement>) => void

Pointer-Events drag pickup on the pane's drag handle (the title-bar grip). Wire this to the handle's onPointerDown; the renderer arms the drag FSM, crosses the pickup threshold, and takes pointer capture on a stable element so reflow moving panes under the cursor can never lose or hijack the drag. Replaces the former HTML5 draggable + onDragStart/onDragEnd plumbing.

onPointerLeave

(event: React.PointerEvent<HTMLElement>) => void

Clears this pane's hover telemetry when the pointer leaves it.

onPointerMove

(event: React.PointerEvent<HTMLElement>) => void

Pre-drag hover telemetry (drop-intent hit-log); inert while a drag is in flight.

onSetSizingMode

(mode: TilingTitleBarSizingMode) => void

Set THIS pane's sizing mode. STATIC modes measure the pane's current bbox (getBoundingClientRect on its [data-leaf-id] element) and pin the chosen dimension(s) to that pixel value; FLEX clears the pin and returns the pane to ratio distribution. Emits via onLayoutChange (controlled).

onToggleCollapse

() => void

Toggle THIS pane's collapse (titlebar-only) state. Collapse pins the leaf's along-parent-axis dimension (see TilingLeafNode.collapsed: height under a stacked parent, width under a side-by-side parent) to the chrome extent (its body is hidden) so its sibling reclaims the freed space; expand restores the pre-collapse sizing. Emits via onLayoutChange (controlled) and fires TilingRendererCommonProps.onPaneCollapsedChange.

onToggleMaximize

() => void

Toggle maximize/restore for this pane.

onToggleMultiSelect

() => void

Toggle THIS pane in/out of the multi-selection set. Wire to an Alt/Opt+click on the pane header. Does not change focus.

paneBodyRenderMode

TilingPaneBodyRenderMode

Canonical pane-body visibility decision resolved by the renderer policy. Keeps custom tile renderers aligned with the default drag/hidden semantics.

paneOrdinal

number

1-based pane ordinal in current tab order (for generic pane labels).

paneWidthPx

number

Current pane viewport width in pixels (for responsive header/control density).

preview

TilingLeafDropPreview | null

The projected landing/result preview for this pane, or null.

seatCount

number

How many workspaces in the set seat this leaf's tile.id (a tile may be seated in several trees at once). 1 in single-layout mode. A pane can use this to show a "shared" affordance or to gate per-workspace state.

surface

readonly

TilingRenderSurface

Which renderer surface these args paint (see TilingRenderSurface). "pane" for the seated in-tree pane; "drag-ghost" for the floating pickup ghost that follows the cursor; "drag-cancel" for the cancel fly-back overlay. On the two drag surfaces every interaction handler is an inert no-op while the capability display flags stay REAL (reflecting the resolved capabilities), so capability-keyed chrome does not vanish mid-drag; a custom pane that wants different drag chrome branches on this discriminator.

tile

TilingTile

The resolved tile payload for this leaf.

widthSizingMode

TilingPaneSizingMode

This pane's current WIDTH sizing mode (static/flexible) — drives the active control state.

workspaceId

string

Id of the workspace whose tree seats this leaf. In single-layout mode this is always TILING_MAIN_WORKSPACE_ID ("main"); in set mode it is the active workspace's id — or, for a pane kept alive by inactiveWorkspaces: "keep-mounted", the (first, in tab order) inactive workspace that seats the tile.

TilingTile

interface

Generic tile payload. Only id + title are required so a product consumer (e.g. a dashboard) can supply a minimal { id, title, content } tile and a custom renderTile. The accent / rows fields drive DefaultTilingTile and the drag-pane snapshot chrome; when omitted they fall back (accent → "cyan", rows → []), so a tile without them renders correctly under the default tile surface. content is the slot a custom renderTile reads. titleBarContent is the optional middle slot between the title and the native pane controls (sizing / maximize / …).

Signature:

interface TilingTile 

Properties

Property

Modifiers

Type

Description

accent?

TilingTileAccent

(Optional) Optional per-tile identity accent. Falls back to "cyan" when omitted.

content?

React.ReactNode

(Optional) Rich body a custom renderTile reads (and the drag ghost paints).

description?

string

(Optional) Optional one-line description surfaced by the default tile chrome.

id

string

Stable tile identity; leaf nodes reference a tile by this id.

rows?

ReadonlyArray<string>

(Optional) Optional text rows for the default tile body. Falls back to [].

title

string

Title shown in the pane header / tab.

titleBarContent?

React.ReactNode

(Optional) Optional custom chrome for the pane titlebar middle slot — rendered between the title (left) and native window controls (right: W/H/max, …). The default tile (and the TilingPaneTitleBarContent primitive for custom renderTile panes) stop pointer-down propagation so interacting with this content does not start a drag. Omit for a title-only header (existing themes unchanged).

TilingTileAccent

type

Per-pane identity accent. A closed, typed palette so a consumer can drive a picker from the enumerable TILING_TILE_ACCENTS list (exported from the renderer) with exhaustive type-checking — adding a member here forces every accent theme map to cover it.

Signature:

type TilingTileAccent = "cyan" | "sky" | "violet" | "indigo" | "emerald" | "amber" | "rose" | "pink";
reference · layout & query

Layout & query

The layout tree node kinds you own in state, the config, and queryTilingLayout for reading structure back out.

DEFAULT_TILING_LAYOUT_CONFIG

variable

Recommended baseline layout config — the generic gap / min-pane / handle scale. config stays a required renderer prop (spacing is explicit at the call site), but consumers that don't tune spacing can spread this for a tasteful, well-readable inter-pane gutter out of the box instead of inventing their own magic numbers. gapPx + handleSizePx together form the visible inter-pane gap (the divider element's hit-target spans the full gapPx + handleSizePx gutter via padding; chrome paints only the center handleSizePx through bg-clip-content. Each child subtracts (gapPx + handleSizePx) / 2 of basis — see splitGapOffsetPx in the split renderer for the balancing math).

Signature:

DEFAULT_TILING_LAYOUT_CONFIG: TilingLayoutConfig

queryTilingLayout()

function

Build a read-only TilingLayoutQuery over layout. The single higher-level layout-inspection helper on the public API: it composes the low-level tree walkers (which stay on the ./engine escape hatch) into the leaf/tile/group/split views and directional-neighbor lookup an app needs to render layout-aware controls.

Signature:

declare function queryTilingLayout(layout: TilingLayoutNode): TilingLayoutQuery;

Parameters

Parameter

Type

Description

layout

TilingLayoutNode

Returns:

TilingLayoutQuery

Example

Inspect the tree you own in state — count panes, list groups, or find a directional neighbor — without walking it by hand:

import { queryTilingLayout, type TilingLayoutNode } from "@n-uf/hypr-tiling";

function paneCount(layout: TilingLayoutNode): number {
  return queryTilingLayout(layout).leafIds.length;
}

const query = queryTilingLayout(layout);
const rightOfFocus = query.neighborLeafId(focusedLeafId, "right");

TilingGroupNode

interface

A group/member slot (HT-GROUP-TABBED-STACKING): N leaves share ONE layout slot as a stacked group with a tab strip — only activeMemberId renders / is hit-tested, the tabs switch the active member. A group is a SLOT (like a leaf), so a slot in either dwindle or master layout can hold a group. Members are leaves only (no nested split/group — a group of one is degenerate and collapses back to a bare leaf).

Signature:

interface TilingGroupNode 

Properties

Property

Modifiers

Type

Description

activeMemberId

string

Always one of members[].id — the single rendered/hit-tested member.

id

string

Stable node identity, unique within the layout tree.

kind

"group"

Node discriminant. Always "group".

members

ReadonlyArray<TilingLeafNode>

≥1 member; array order is the tab order.

sizing?

TilingPaneSizing

(Optional) A group can be static like a leaf (per-dimension static/flexible sizing).

TilingLayoutConfig

interface

Global geometry configuration for the renderer (the config prop).

Signature:

interface TilingLayoutConfig 

Properties

Property

Modifiers

Type

Description

collapseBodyMode?

TilingCollapseBodyMode

(Optional) Collapsed pane body mount policy (HT-COLLAPSE-BODY-MODE) — see TilingCollapseBodyMode. Undefined → "keep-mounted".

collapsedExtentPx?

number

(Optional) Collapsed titlebar-only extent (CSS px) — the along-parent-split-axis extent a leaf is pinned to when COLLAPSED (HT-PANE-COLLAPSE): height under a stacked (vertical) parent, width under a side-by-side (horizontal) parent (see TilingLeafNode.collapsed). Sized to the pane's chrome/header extent so a collapsed pane shows just its title bar. Undefined → TILING_DEFAULT_COLLAPSED_EXTENT_PX. Set it to match your custom header extent when it differs from the default.

gapPx

number

Gap (CSS px) painted in the gutters between panes.

handleSizePx

number

Resize-handle chrome thickness (CSS px). The interactive hit-target spans the full boundary gutter (gapPx + handleSizePx); this value is the painted center strip when resizeHandlesVisible is on.

minPaneSizePx

number

Library-wide default minimum pane extent (CSS px) a resize divider will not shrink a pane below — the LAST-resort fallback in the min-pane precedence chain, behind a direct child leaf's own TilingLeafNode.minBBoxPx (along-axis component) and behind TilingSplitNode.minPaneSizePx.

resizeFloor?

TilingResizeFloor

(Optional) Library-wide default for which floor an interactive resize bounds a pane to (HT-RESIZE-FLOOR) — a direct-child leaf's own TilingLeafNode.resizeFloor wins over this. Undefined → "chrome" (HT-RESIZE-FLOOR-DEFAULT, size-out to the collapsed titlebar extent). Set "body" here to restore the old content-floor behavior library-wide. See TilingResizeFloor for the two modes.

TilingLayoutNode

type

A node in the layout tree: a leaf (single pane), a split (two subtrees), or a group (tabbed leaves in one slot). The recursive union you own in state and feed to TilingRenderer via the layout prop.

Signature:

type TilingLayoutNode = TilingLeafNode | TilingSplitNode | TilingGroupNode;

References: TilingLeafNode, TilingSplitNode, TilingGroupNode

TilingLayoutQuery

interface

A read-only structural view of a TilingLayoutNode tree, aggregating the queries an app typically needs to drive layout-aware UI (shortcut chips, pane counters, directional focus) without walking the tree by hand or reaching for the low-level ./engine walkers.

Signature:

interface TilingLayoutQuery 

Properties

Property

Modifiers

Type

Description

groups

readonly

ReadonlyArray<TilingGroupNode>

Every group in the tree (depth-first, reading order) — the tab-strip source.

hasMasterSplit

readonly

boolean

Whether any split node is in "master" layout mode.

leafIds

readonly

ReadonlyArray<string>

The leaf ids the OUTER layout sees, in reading order — a leaf contributes its id; a group contributes ONLY its active member id (the group presents as one pane to the outer layout).

neighborLeafId

readonly

(fromLeafId: string, direction: TilingFocusDirection) => string | null

The leaf id reached from fromLeafId in the given direction (spatial neighbor by pane geometry), or null when there is no pane that way.

splits

readonly

ReadonlyArray<TilingSplitNode>

Every split node in the tree (depth-first, reading order).

tileOrder

readonly

ReadonlyArray<string>

The tile ids the tree renders, in reading order (group members reported in tab order). Drives tile-order-dependent UI such as a tab strip.

TilingLeafNode

interface

A leaf slot: a single pane that renders one tile. The renderer's smallest addressable layout unit — focus, drag, resize, and sizing all target a leaf by its id.

Signature:

interface TilingLeafNode 

Properties

Property

Modifiers

Type

Description

collapsed?

boolean

(Optional) Whether this leaf is COLLAPSED to titlebar-only (HT-PANE-COLLAPSE, axis-aware). A collapsed leaf is pinned STATIC, to the chrome/titlebar extent (collapsedExtentPx, see TilingLayoutConfig), in the dimension that runs ALONG its immediate PARENT split's axis:

  • parent vertical (stacked) → pins height; its flexible sibling reclaims the freed height. - parent horizontal (side-by-side) → pins width; its flexible sibling reclaims the freed width.

This mirrors the split's own along-axis/cross-axis convention (isStaticAlongSplitAxis) rather than always pinning height, so a side-by-side collapse actually narrows the pane instead of leaving its full column width reserved. Its body is not painted. Undefined → expanded (the default). Persisted in the layout tree alongside sizing, so createPersistedTilingLayout round-trips it and integrity stays sensible.

collapsedDimension?

TilingDimension

(Optional) Which dimension collapsed pinned — the along-axis dimension of the immediate parent split AT THE MOMENT this leaf collapsed. Recorded explicitly (rather than re-derived from sizing at read time) so a consumer can render axis-appropriate chrome (e.g. a narrow vertical titlebar for a "width" collapse vs. a short horizontal one for "height") without guessing from pixel values, which would be ambiguous if an unrelated cross-axis static pin happens to equal collapsedExtentPx. Cleared on expand. Only meaningful while collapsed === true; exposed to custom renderTile panes as TilingRenderTileProps.collapsedDimension.

collapsedRestore?

TilingPaneSizing

(Optional) The pre-collapse TilingPaneSizing snapshot, captured when the leaf is collapsed and restored verbatim on expand (so the leaf returns to its exact prior flexible/static contribution). Undefined when the leaf was flexible before collapse (expand then clears the pin → flexible) or when the leaf is not collapsed. Only meaningful while collapsed === true.

id

string

Stable node identity, unique within the layout tree.

kind

"leaf"

Node discriminant. Always "leaf".

minBBoxPx?

TilingMinBBoxPx

(Optional) Leaf-scoped resize floor (CSS px) — a per-pane along-axis minimum that TRAVELS WITH the pane across rearrange, because it is stored on the leaf node itself rather than on a split boundary. When this leaf sits as a DIRECT child of a split, the component that runs ALONG that split's axis (widthPx for a "horizontal" split, heightPx for a "vertical" split) is the effective floor for that boundary — resolved with this precedence (most specific wins):

  1. leaf.minBBoxPx[alongDimension] (this field) — travels with the pane. 2. TilingSplitNode.minPaneSizePx — pinned to that split boundary. 3. TilingLayoutConfig.minPaneSizePx — the library-wide default.

An undefined or non-positive per-dimension value falls through to the next precedence level. The cross-axis component is reserved for a future cross-axis floor; today's clamp pipeline only bounds the split-axis ratio, so only the along-axis component is consulted. Persisted alongside sizing so createPersistedTilingLayout round-trips it.

resizeFloor?

TilingResizeFloor

(Optional) Per-leaf override of which floor an interactive resize bounds this pane to (HT-RESIZE-FLOOR) — wins over TilingLayoutConfig.resizeFloor. Undefined falls through to the config default, itself defaulting to "chrome" (HT-RESIZE-FLOOR-DEFAULT). Set "body" here to opt this leaf back into the content floor. See TilingResizeFloor for the two modes.

sizing?

TilingPaneSizing

(Optional) Per-dimension static/flexible sizing. Undefined dimensions are flexible.

tileId

string

The TilingTile.id this leaf renders.

TilingSplitNode

interface

A binary split node: divides its region between two child subtrees along axis by ratio. In layoutMode: "master" the binary structure defines slot membership/order while the master resolver drives geometry.

Signature:

interface TilingSplitNode 

Properties

Property

Modifiers

Type

Description

axis

TilingSplitAxis

Split orientation ("horizontal" = side-by-side, "vertical" = stacked).

first

TilingLayoutNode

The first (top/left) child subtree.

gapPx?

number

(Optional) Optional per-split gap override (CSS px) between the two children.

id

string

Stable node identity, unique within the layout tree.

kind

"split"

Node discriminant. Always "split".

layoutMode?

TilingLayoutMode

(Optional) Arrangement of this subtree's slots. Undefined → "dwindle" (the binary split layout — every hand-authored tree + the test baseline is unchanged). "master" lays the subtree's descendant slots out as master area + stack.

masterCount?

number

(Optional) layoutMode: "master" only — number of slots in the master area. Undefined → 1. Clamped to [1, slotCount] by the resolver / reducers.

masterOrientation?

TilingMasterOrientation

(Optional) layoutMode: "master" only — where the master area sits. Undefined → "left". In master mode the split's ratio is reused as the master-area fraction along this orientation's primary axis.

minPaneSizePx?

number

(Optional) Optional per-split minimum pane extent (CSS px) enforced on resize. Sits in the MIDDLE of the min-pane precedence chain: a direct child leaf's own TilingLeafNode.minBBoxPx (along-axis component) wins over this; this wins over TilingLayoutConfig.minPaneSizePx. Unlike minBBoxPx, this floor is pinned to THIS split boundary and does not travel with either child across rearrange.

ratio

number

Fraction [0, 1] of the region allotted to first along axis.

second

TilingLayoutNode

The second (bottom/right) child subtree.

sizing?

TilingPaneSizing

(Optional) Per-dimension static/flexible sizing. Undefined dimensions are flexible.

reference · theming

Theming

Built-in themes, per-pane accents, the theme provider, and useTilingTheme for reading tokens inside a custom pane.

DEFAULT_TILING_THEME_ID

variable

Default library theme id (preserves the prior look at the round-1 checkpoint).

Signature:

DEFAULT_TILING_THEME_ID: TilingThemeId

resolveTilingTheme()

function

Resolve a theme id (or the default) to its TilingTheme.

Signature:

declare function resolveTilingTheme(themeId: TilingThemeId | undefined): TilingTheme;

Parameters

Parameter

Type

Description

themeId

TilingThemeId | undefined

Returns:

TilingTheme

TILING_THEMES

variable

Ordered, enumerable theme list — the generic capability a theme switcher iterates.

Signature:

TILING_THEMES: readonly TilingTheme[]

TilingTheme

interface

A complete theme: the static surface token groups plus the accent-composition resolvers. The resolvers are the contract for "how per-pane accents compose with the theme" — each theme decides how much of an accent's hue/glow it spends on the resting surface, title text, focus frame, and active tab.

Signature:

interface TilingTheme 

Properties

Property

Modifiers

Type

Description

divider

readonly

TilingThemeDividerTokens

Split-divider / gap handle tokens.

dragChrome?

readonly

Partial<TilingThemeDragChromeTokens>

(Optional) Drag-state chrome tokens (ghost wrapper delta, seat, source dim, drop target, cursor badge). Optional + partial — omitted tokens resolve through resolveDragChrome(theme) to a pane-shell-inheriting default.

ghost

readonly

TilingThemeGhostTokens

Drag-ghost shell tokens.

ghostChip?

readonly

(ctx: TilingGhostChipContext) => React.ReactNode

(Optional) Compact-ghost chip CONTENTS. The renderer wraps the return in the cursor-anchored chip shell (data-drag-ghost-chip); this slot paints the label. Default is a minimal neutral chip (tile title, else leaf id). Optional — omitted themes keep the built-in chip.

id

readonly

TilingThemeId | (string & {})

The theme's id. A built-in theme uses a TilingThemeId member; a consumer-authored theme mints its own id string (the string & {} widening keeps built-in ids autocompletable while admitting any id). TILING_THEME_REGISTRY stays keyed by the closed TilingThemeId union, so the compile-time built-in coverage guarantee is unaffected.

label

readonly

string

Human-readable theme label (e.g. for a theme picker).

paneHeader

readonly

TilingThemePaneHeaderTokens

Pane header chrome tokens.

paneShell

readonly

TilingThemePaneShellTokens

Pane host shell surface + interaction-state tokens.

resolveAccentText

readonly

(accent: TilingTileAccent | undefined) => string

Accent title-text color.

resolveFocusFrame

readonly

(accent: TilingTileAccent | undefined) => string

Full focused-pane frame. Prefer a SINGLE radius-following stroke (box-shadow / one border-* recolor) — stacking border + ring (or outline) at the corners produces square tick artifacts on rounded panes.

resolvePaneAccentSurface

readonly

(accent: TilingTileAccent | undefined) => string

Resting pane accent composition (border tint + colored shadow).

resolveTabActive

readonly

(accent: TilingTileAccent | undefined) => string

Active tab / switcher / group-member chip.

root

readonly

TilingThemeRootTokens

Renderer-root + viewport surface tokens.

topBar

readonly

TilingThemeTopBarTokens

Top-bar / tab-strip chrome tokens.

workspaceTab?

readonly

string

(Optional) Every workspace tab element's resting classes (tabProps.className).

workspaceTabActive?

readonly

string

(Optional) Composed onto the ACTIVE workspace tab (aria-selected="true").

workspaceTabDropTarget?

readonly

string

(Optional) Composed onto the tab a dragged leaf is hovering (data-drop-target).

workspaceTabs?

readonly

string

(Optional) Workspace tab-strip tablist element classes, consumed by the headless useTilingWorkspaceTabs / TilingWorkspaceTabs (tablistProps.className). Optional — the strip is UNSTYLED until a theme (or the host) supplies the workspaceTab* tokens, so a theme that omits them never shows a look it did not author.

workspaceTransition?

readonly

Partial<TilingThemeWorkspaceTransitionTokens>

(Optional) Workspace-switch slide / fade timing. Optional + partial — omitted tokens resolve through resolveWorkspaceTransition to 200 ms and the drag-hop easing. Built-in themes omit the slot (library defaults).

TilingThemeId

type

Closed set of built-in visual theme ids for the renderer. A theme bundles the class-string tokens for every surface (pane shell, header, focus frame, ghost, dividers, root, tab strip) plus the accent-composition resolvers. The token interfaces + registry live in ./theme; this union is the central contract a consumer types its themeId state against.

Signature:

type TilingThemeId = "neon-terminal" | "clean-flat" | "mosaic";

TilingThemeProvider()

function

Provides the active theme to every renderer subcomponent.

Signature:

declare function TilingThemeProvider(input: {
    theme: TilingTheme;
    children: React.ReactNode;
}): React.ReactElement;

Parameters

Parameter

Type

Description

{ theme, children, }

(not declared)

input

{ theme: TilingTheme; children: React.ReactNode; }

Returns:

React.ReactElement

useTilingTheme()

function

Reads the active theme from context (defaults to the library default).

Signature:

declare function useTilingTheme(): TilingTheme;

Returns:

TilingTheme

reference · commands

Commands

The imperative command handle and the typed command union you dispatch from your own controls.

TilingCommand

type

The public, typed, dispatch-style command set — the public API's imperative entry point (the Hyprland dispatch analog). Every internal renderer action is enumerated here so an embedding app can invoke tiler behavior programmatically (via the TilingCommandHandle) and so a keyboard binding can target any of them (TilingKeyBinding).

Context-dependent commands carry an OPTIONAL leafId; when omitted the renderer resolves it against the CURRENT focused leaf at dispatch time (the "act on the focused pane" ergonomic). Commands that need explicit operands (swap / insert / split ops) require them.

Signature:

type TilingCommand = {
    kind: "focus-pane";
    leafId: string;
} | {
    kind: "focus-direction";
    direction: TilingFocusDirection;
} | {
    kind: "focus-cycle";
    direction: TilingPaneCycleDirection;
} | {
    kind: "focus-jump";
    paneNumber: number;
} | {
    kind: "focus-current-or-last";
} | {
    kind: "toggle-maximize";
    leafId?: string;
} | {
    kind: "maximize";
    leafId?: string;
} | {
    kind: "restore";
} | {
    kind: "enter-move-mode";
    leafId?: string;
} | {
    kind: "move-aim";
    direction: TilingFocusDirection;
} | {
    kind: "commit-move-mode";
} | {
    kind: "cancel-move-mode";
} | {
    kind: "swap-panes";
    sourceLeafId: string;
    targetLeafId: string;
} | {
    kind: "insert-adjacent";
    sourceLeafId: string;
    targetLeafId: string;
    placement: TilingMovePlacement;
} | {
    kind: "acquire-space";
    leafId?: string;
    direction: TilingFocusDirection;
} | {
    kind: "set-sizing";
    leafId?: string;
    mode: TilingTitleBarSizingMode;
} | {
    kind: "toggle-collapse";
    leafId?: string;
} | {
    kind: "set-collapsed";
    leafId?: string;
    collapsed: boolean;
} | {
    kind: "set-split-ratio";
    splitId: string;
    ratio: number;
} | {
    kind: "toggle-split-axis";
    splitId: string;
} | {
    kind: "set-layout-mode";
    splitId?: string;
    mode: TilingLayoutMode;
} | {
    kind: "cycle-layout-mode";
    splitId?: string;
} | {
    kind: "set-master-count";
    splitId?: string;
    count: number;
} | {
    kind: "adjust-master-count";
    splitId?: string;
    delta: number;
} | {
    kind: "set-master-orientation";
    splitId?: string;
    orientation: TilingMasterOrientation;
} | {
    kind: "cycle-master-orientation";
    splitId?: string;
} | {
    kind: "adjust-master-ratio";
    splitId?: string;
    delta: number;
} | {
    kind: "group-leaves";
    leafIds: ReadonlyArray<string>;
    hostLeafId?: string;
} | {
    kind: "toggle-group";
    leafId?: string;
} | {
    kind: "ungroup";
    groupId?: string;
} | {
    kind: "add-to-group";
    groupId: string;
    sourceLeafId: string;
} | {
    kind: "remove-from-group";
    groupId: string;
    memberId: string;
} | {
    kind: "group-tab-cycle";
    groupId?: string;
    direction: TilingPaneCycleDirection;
} | {
    kind: "group-tab-jump";
    groupId?: string;
    memberNumber: number;
} | {
    kind: "switch-workspace";
    workspaceId: string;
}
/**
 * Switch to the 1-based n-th workspace in tab order (`Alt+1` → `index: 1`).
 * Bindable without a set at key-binding construction time; the wrapper
 * resolves `index` against the live `workspaces` array at dispatch.
 */
 | {
    kind: "switch-workspace";
    index: number;
} | {
    kind: "cycle-workspace";
    direction: TilingPaneCycleDirection;
} | {
    kind: "move-leaf-to-workspace";
    leafId?: string;
    workspaceId: string;
    placement?: TilingWorkspacePlacement;
    follow?: boolean;
}
/**
 * Move the focused (or explicit) leaf to the previous / next workspace in
 * tab order. Neighbour is resolved at dispatch so the binding stays valid
 * after `activeId` changes (unlike a bind-time `workspaceId`).
 */
 | {
    kind: "move-leaf-to-workspace";
    leafId?: string;
    direction: TilingPaneCycleDirection;
    placement?: TilingWorkspacePlacement;
    follow?: boolean;
} | {
    kind: "reveal-tile";
    tileId: string;
}
/**
 * Replace one workspace's `layout` (and `name`) from
 * {@link TilingRendererWorkspaceSetProps.workspaceDefaults}. `workspaceId`
 * omitted → the active workspace. `activeId` is unchanged (no switch
 * transition). No-op without `workspaceDefaults`. Host opt-in — no default
 * key binding.
 */
 | {
    kind: "reset-workspace";
    workspaceId?: string;
}
/**
 * Replace the whole set from
 * {@link TilingRendererWorkspaceSetProps.workspaceDefaults}, keeping the
 * current `activeId` when that id exists in the seed. No-op without
 * `workspaceDefaults`. Host opt-in — no default key binding.
 */
 | {
    kind: "reset-workspaces";
};

References: TilingFocusDirection, TilingPaneCycleDirection, TilingMovePlacement, TilingTitleBarSizingMode, TilingLayoutMode, TilingMasterOrientation, TilingWorkspacePlacement

TilingCommandHandle

interface

The imperative handle exposed via ref on TilingRenderer. A consumer holds the ref and drives the tiler programmatically — dispatch routes a command through the SAME internal router the keyboard layer uses, so a disabled-capability command is a safe no-op (it never mutates the layout).

Signature:

interface TilingCommandHandle 

Properties

Property

Modifiers

Type

Description

dispatch

(command: TilingCommand) => void

Route a TilingCommand through the renderer's command router.

reference · advanced helpers

Advanced helpers

Power-user helpers and the resolved / capability / keymap / debug type surface — isCommandEnabled, the interaction-capability shapes, and the query utilities you reach for only when building custom command bars, keyboard layers, and observability. Each carries a consumer-usage @example in its hover-docs.

activeWorkspace()

function

The workspace set.activeId names, or null when it names none (an integrity issue).

Signature:

declare function activeWorkspace(set: TilingWorkspaceSet): TilingWorkspace | null;

Parameters

Parameter

Type

Description

set

TilingWorkspaceSet

Returns:

TilingWorkspace | null

adoptIncomingWorkspaceTrees()

function

Adopt a host value. Drop local / pending trees for workspaces whose incoming layout differs from last committed (the host is authoritative). Keep overlays whose incoming layout still matches last committed (host echo of our last persist). Optionally drop entries for workspaces the host no longer has.

Signature:

declare function adoptIncomingWorkspaceTrees(input: AdoptIncomingWorkspaceTreesInput): AdoptIncomingWorkspaceTreesResult;

Parameters

Parameter

Type

Description

input

AdoptIncomingWorkspaceTreesInput

Returns:

AdoptIncomingWorkspaceTreesResult

AdoptIncomingWorkspaceTreesInput

interface

Input to adoptIncomingWorkspaceTrees().

Signature:

interface AdoptIncomingWorkspaceTreesInput 

Properties

Property

Modifiers

Type

Description

dropMissingWorkspaces?

readonly

boolean

(Optional) When true (default), drop last-committed / local / pending entries for workspaces the host no longer has. Pass false while the controller is ahead of the host (stale) so a just-created workspace is not forgotten.

incoming

readonly

TilingWorkspaceSet

The host's current set.

lastCommitted

readonly

TilingWorkspaceTreeMap

Per-workspace layouts of the last committed (or adopted) set.

localTrees

readonly

TilingWorkspaceTreeMap

Local tree overlays (edited since last persist / adopt).

pending

readonly

TilingWorkspaceTreeMap

Trees awaiting a debounced "tree" commit.

AdoptIncomingWorkspaceTreesResult

interface

Result of adoptIncomingWorkspaceTrees().

Signature:

interface AdoptIncomingWorkspaceTreesResult 

Properties

Property

Modifiers

Type

Description

droppedPending

readonly

boolean

true when at least one pending tree was dropped (host superseded it).

lastCommitted

readonly

Map<TilingWorkspaceId, TilingLayoutNode | null>

Last-committed layouts after adopting the host's trees.

localTrees

readonly

Map<TilingWorkspaceId, TilingLayoutNode | null>

Local overlays after dropping superseded / released trees.

localTreesChanged

readonly

boolean

true when localTrees changed (a setState is needed).

pending

readonly

Map<TilingWorkspaceId, TilingLayoutNode | null>

Pending map after dropping superseded trees.

canElementScrollFurther()

function

Whether ONE element can still scroll along axis in direction: its computed overflow-{x,y} lets the user scroll, its scroll extent exceeds its client extent, and its scroll offset is not already at the bound the direction heads to. On X the RTL sign convention is honoured (a direction: rtl scroller reports scrollLeft in [-(scrollWidth − clientWidth), 0], so "visually right" is scrollLeft → 0).

Signature:

declare function canElementScrollFurther(element: HTMLElement, axis: ScrollChainAxis, direction: ScrollChainDirection): boolean;

Parameters

Parameter

Type

Description

element

HTMLElement

axis

ScrollChainAxis

direction

ScrollChainDirection

Returns:

boolean

CapturedViewClone

interface

Result of captureViewClone().

Signature:

interface CapturedViewClone 

Properties

Property

Modifiers

Type

Description

degraded

readonly

boolean

true when a canvas bitmap could not be copied (tainted drawImage, getContext("2d") missing, canvas over VIEW_CAPTURE_CANVAS_PIXEL_BUDGET, or source/clone canvas counts differ). Caller should resolve "slide" to "fade".

node

readonly

HTMLElement

Inert clone of root (ids / hit-test attrs stripped; canvases copied).

captureViewClone()

function

Freeze root (typically the set-mode viewport) as an inert overlay clone.

Steps: cloneNode(true) → drop existing transition layers → copy canvas bitmaps → strip id + hit-test data-* → pointer-events: none, aria-hidden, inert. A failed / oversized canvas sets degraded.

Signature:

declare function captureViewClone(root: HTMLElement): CapturedViewClone;

Parameters

Parameter

Type

Description

root

HTMLElement

Returns:

CapturedViewClone

clampUnitProgress()

function

Clamp a progress sample into unit [0, 1]. NaN collapses to 0.

Signature:

declare function clampUnitProgress(progress: number): number;

Parameters

Parameter

Type

Description

progress

number

Returns:

number

classifyIncomingWorkspaceSet()

function

Classify an incoming host value against the last committed set. See IncomingWorkspaceSetKind.

Signature:

declare function classifyIncomingWorkspaceSet(incoming: TilingWorkspaceSet, lastCommitted: TilingWorkspaceSet, ahead: boolean): IncomingWorkspaceSetKind;

Parameters

Parameter

Type

Description

incoming

TilingWorkspaceSet

lastCommitted

TilingWorkspaceSet

ahead

boolean

Returns:

IncomingWorkspaceSetKind

clientRectContains()

function

Whether point lies inside rect (edges inclusive).

Signature:

declare function clientRectContains(rect: TilingClientRect, point: TilingClientPoint): boolean;

Parameters

Parameter

Type

Description

rect

TilingClientRect

point

TilingClientPoint

Returns:

boolean

createDomScrollChainPort()

function

Default DOM-backed ScrollChainPort: walks from element up through its ancestors to the viewport root (inclusive) and reports true as soon as one of them canElementScrollFurther(). Text nodes / non-HTML targets start from their parent element; a target outside the root walks to the document root. false when the root is unmounted or element is null.

Signature:

declare function createDomScrollChainPort(rootRef: ElementRef<HTMLElement | null>): ScrollChainPort<EventTarget>;

Parameters

Parameter

Type

Description

rootRef

ElementRef<HTMLElement | null>

Returns:

ScrollChainPort<EventTarget>

createDomWheelTouchPort()

function

Default DOM-backed WheelTouchInputPort over one viewport root element.

  • wheel: a wheel listener on element. Registered passive: true at rest so page scrolling is never blocked; setTracking(true) re-registers it passive: false and every sample then calls preventDefault() so the browser's own horizontal handling (history back-swipe, ancestor scroll) stays out of a gesture the FSM owns. setTracking(false) restores the passive listener. - touch: pointerdown (pointerType === "touch") on element starts a gesture; pointermove / pointerup / pointercancel on window follow that one pointerId until it ends. The scroll chain is judged from the TOUCH-START target for the finger's current direction on every move.

Samples carry element.clientWidth as widthPx, or null when the element has no laid-out width (so the FSM keeps its context / config width).

Signature:

declare function createDomWheelTouchPort(element: HTMLElement, options: DomWheelTouchPortOptions): WheelTouchInputPort;

Parameters

Parameter

Type

Description

element

HTMLElement

options

DomWheelTouchPortOptions

Returns:

WheelTouchInputPort

createPersistedTilingLayout()

function

Build a PersistedTilingLayout — a thin persist adapter over the engine's assertLayoutIntegrity / repairLayout. The storage key, expected tile ids, and fallback tree stay host-owned; repair stays engine-owned.

Signature:

declare function createPersistedTilingLayout(options: CreatePersistedTilingLayoutOptions): PersistedTilingLayout;

Parameters

Parameter

Type

Description

options

CreatePersistedTilingLayoutOptions

CreatePersistedTilingLayoutOptions

Returns:

PersistedTilingLayout

Example

const persisted = createPersistedTilingLayout({
  storageKey: "annotate.tilingLayout",
  expectedTileIds: ["cases", "document", "review"],
  fallbackLayout: DEFAULT_LAYOUT,
  config: TILING_CONFIG,
});
const [layout, setLayout] = useState(() => persisted.load());
// onLayoutChange: setLayout(persisted.commit(next));

CreatePersistedTilingLayoutOptions

interface

Options for createPersistedTilingLayout().

Signature:

interface CreatePersistedTilingLayoutOptions 

Properties

Property

Modifiers

Type

Description

config

TilingLayoutConfig

Gap / min-pane / handle geometry, forwarded to the repair pass.

expectedTileIds

ReadonlyArray<string>

Host tile ids that must appear exactly once across the persisted tree. Forwarded verbatim to assertLayoutIntegrity / repairLayout; a persisted tree that is missing/duplicating/adding tiles is rebuilt from CreatePersistedTilingLayoutOptions.fallbackLayout.

fallbackLayout

TilingLayoutNode

Host-authored replacement tree used when the persisted value is absent, unparseable, or fails hard integrity (requiresRebuild). Stays app-owned; the adapter never invents a layout.

resolveContainerSize?

() => TilingLayoutContainerSize

(Optional) Resolves the current container geometry for the repair pass. Called on every load / commit so a resized viewport heals correctly. Defaults to reading window.innerWidth / innerHeight (falling back to 1280×800 when no DOM / degenerate size is available).

storage?

TilingLayoutStorage

(Optional) Key/value store to persist into. Defaults to window.localStorage when a DOM is present, otherwise an in-memory store (SSR-safe no-op persistence).

storageKey

string

Storage key the serialized layout tree is read from / written to.

createWorkspace()

function

Append (or insert at at) a workspace. Unchanged when the id is empty or taken, the name is invalid, or the set already holds TILING_WORKSPACES_MAX workspaces.

Signature:

declare function createWorkspace(set: TilingWorkspaceSet, input: CreateWorkspaceInput): TilingWorkspaceSet;

Parameters

Parameter

Type

Description

set

TilingWorkspaceSet

input

CreateWorkspaceInput

Returns:

TilingWorkspaceSet

CreateWorkspaceInput

interface

Input to createWorkspace().

Signature:

interface CreateWorkspaceInput 

Properties

Property

Modifiers

Type

Description

activate?

readonly

boolean

(Optional) Make the new workspace active. Default false.

at?

readonly

number

(Optional) Insertion index in tab order. Default: append. Clamped to [0, length].

id

readonly

TilingWorkspaceId

Unique within the set. Refused (unchanged set) when empty or already present.

layout?

readonly

TilingLayoutNode | null

(Optional) Initial tree. Default null (empty). A leaf already seated in another workspace is MOVED here (replace wins) so no tree ends up with a stale second seat the host did not ask for; use showInWorkspace() to share.

name

readonly

string

Display name; refused when it normalises to null (see normalizeWorkspaceName()).

cycleWorkspace()

function

Activate the next / previous workspace in tab order (wraps). Unchanged with one workspace.

Signature:

declare function cycleWorkspace(set: TilingWorkspaceSet, direction: TilingPaneCycleDirection): TilingWorkspaceSet;

Parameters

Parameter

Type

Description

set

TilingWorkspaceSet

direction

TilingPaneCycleDirection

Returns:

TilingWorkspaceSet

DEFAULT_DRAG_ANIMATION_SPEED_PERCENT

variable

Default drag animation speed percent (100 = the baseline duration).

Signature:

DEFAULT_DRAG_ANIMATION_SPEED_PERCENT: number

DEFAULT_DRAG_HOP_EASING

variable

The default ghost hop / pickup / hop-out timing function (a snappy decel).

Signature:

DEFAULT_DRAG_HOP_EASING: string

DEFAULT_DRAG_REFLOW_EASING

variable

The default survivor-reflow settle timing function. Equals the hop curve so the ghost and the survivors read as one coordinated motion (the renderer's dragReflowEasing prop falls back to the resolved dragHopEasing when undefined, so this default only applies when BOTH are unset).

Signature:

DEFAULT_DRAG_REFLOW_EASING: string

DEFAULT_TILE_ACCENT

variable

First palette member — the fallback when a tile omits accent.

Signature:

DEFAULT_TILE_ACCENT: TilingTileAccent

DEFAULT_WORKSPACE_TRANSITION_CONFIG

variable

Default config: no motion until a host opts in (interaction.workspaces.switch.transition, follow-up wiring).

Signature:

DEFAULT_WORKSPACE_TRANSITION_CONFIG: TilingWorkspaceTransitionConfig

DEFAULT_WORKSPACE_TRANSITION_DURATION_MS

variable

Default duration for a timed workspace-switch settle.

Signature:

DEFAULT_WORKSPACE_TRANSITION_DURATION_MS: number

DEFAULT_WORKSPACE_TRANSITION_EASING

variable

Default easing — the same snappy decel as DEFAULT_DRAG_HOP_EASING so workspace switches share the drag-chrome motion language.

Signature:

DEFAULT_WORKSPACE_TRANSITION_EASING: string

deleteWorkspace()

function

Delete a workspace. The LAST workspace is never deleted (refused, unchanged set). Leaves shown only there leave the set (reported as removedTileIds); leaves also shown elsewhere keep their other seats. When the deleted workspace was active, the previous one in tab order becomes active, else the next.

Signature:

declare function deleteWorkspace(set: TilingWorkspaceSet, id: TilingWorkspaceId): TilingDeleteWorkspaceResult;

Parameters

Parameter

Type

Description

set

TilingWorkspaceSet

id

TilingWorkspaceId

Returns:

TilingDeleteWorkspaceResult

diffWorkspaceTreeLayouts()

function

Workspaces in next whose layout reference differs from the same id in current. New workspaces (no prior id) are ignored — membership changes are lifecycle, not tree edits.

Signature:

declare function diffWorkspaceTreeLayouts(current: TilingWorkspaceSet, next: TilingWorkspaceSet): ReadonlyArray<TilingWorkspaceTreeDiff>;

Parameters

Parameter

Type

Description

current

TilingWorkspaceSet

next

TilingWorkspaceSet

Returns:

ReadonlyArray<TilingWorkspaceTreeDiff>

DomWheelTouchPortOptions

interface

Options of createDomWheelTouchPort().

Signature:

interface DomWheelTouchPortOptions 

Properties

Property

Modifiers

Type

Description

now?

() => number

(Optional) Monotonic clock (ms); default performance.now().

scrollChain

ScrollChainPort<EventTarget>

Scroll-chain oracle for canScrollFurther (createDomScrollChainPort over the same root).

touch

boolean

Attach the touch (pointer* with pointerType === "touch") listeners.

wheel

boolean

Attach the wheel listener.

DRAG_ANIMATION_SPEED_MAX_PERCENT

variable

Fastest (ceiling) drag animation speed percent the slider + clamp allow.

Signature:

DRAG_ANIMATION_SPEED_MAX_PERCENT: number

DRAG_ANIMATION_SPEED_MIN_PERCENT

variable

Slowest (floor) drag animation speed percent the slider + clamp allow.

Signature:

DRAG_ANIMATION_SPEED_MIN_PERCENT: number

findWorkspaceById()

function

The workspace with id, or null when absent.

Signature:

declare function findWorkspaceById(set: TilingWorkspaceSet, id: TilingWorkspaceId): TilingWorkspace | null;

Parameters

Parameter

Type

Description

set

TilingWorkspaceSet

id

TilingWorkspaceId

Returns:

TilingWorkspace | null

foldPendingTrees()

function

Replace matching workspaces' trees with the pending overlay. Same set reference when pending is empty or every overlay is already the seated tree (setWorkspaceLayout same-ref discipline).

Signature:

declare function foldPendingTrees(set: TilingWorkspaceSet, pending: TilingWorkspaceTreeMap): TilingWorkspaceSet;

Parameters

Parameter

Type

Description

set

TilingWorkspaceSet

pending

TilingWorkspaceTreeMap

Returns:

TilingWorkspaceSet

hideFromWorkspace()

function

Remove a leaf's seat from workspace from (DashAI hideItem). When that was its only seat the leaf leaves the set — consult queryWorkspaceSet().workspacesOfLeaf first if the host must keep every item shown somewhere. Unchanged when from does not show the leaf.

Signature:

declare function hideFromWorkspace(set: TilingWorkspaceSet, leafId: string, from: TilingWorkspaceId): TilingWorkspaceSet;

Parameters

Parameter

Type

Description

set

TilingWorkspaceSet

leafId

string

from

TilingWorkspaceId

Returns:

TilingWorkspaceSet

hideTileFromWorkspace()

function

Remove a tile's seat from workspace from. orphaned is true when the tile now has no seat anywhere (including when it was already seated nowhere). Unchanged (same set reference) when from does not show the tile.

Signature:

declare function hideTileFromWorkspace(set: TilingWorkspaceSet, tileId: string, from: TilingWorkspaceId): TilingHideTileResult;

Parameters

Parameter

Type

Description

set

TilingWorkspaceSet

tileId

string

from

TilingWorkspaceId

Returns:

TilingHideTileResult

IncomingWorkspaceSetKind

type

How an incoming host value relates to the last set the controller committed.

  • echo — same workspaces, names, layout refs, and activeId as last committed (the host applied our persist). Keep local overlays that still differ; release ones the host has caught up with. - stale — we are ahead (a commit the host has not applied yet): shared workspace layouts still match, but membership / names / activeId do not. Keep the optimistic set. - authoritative — at least one incoming layout differs from last committed, or we are not ahead and the set is not an echo. The host document wins; drop local trees for those workspaces.

Signature:

type IncomingWorkspaceSetKind = "echo" | "stale" | "authoritative";

isCommandEnabled()

function

Whether command would do anything given the current capability gates: true when the command's required capability is enabled (or it requires none). A keyboard binding to a disabled-capability command stays browser-graceful (the caller does not preventDefault); an imperative dispatch of one is a no-op.

Signature:

declare function isCommandEnabled(command: TilingCommand, gates: TilingCommandGates): boolean;

Parameters

Parameter

Type

Description

command

TilingCommand

gates

TilingCommandGates

Returns:

boolean

Example

Build your own command bar / keyboard shortcut that only fires (and only renders its button) when the target command is actually enabled — the gate the renderer itself uses for its shortcut chips:

import {
  resolveInteractionCapabilities,
  isCommandEnabled,
  type TilingCommand,
  type TilingCommandGates,
  type TilingCommandHandle,
  type TilingInteractionCapabilities,
} from "@n-uf/hypr-tiling";

function gatesFor(interaction?: TilingInteractionCapabilities): TilingCommandGates {
  const caps = resolveInteractionCapabilities(interaction);
  return {
    maximizeEnabled: caps.maximize.enable,
    paneSwitchingEnabled: caps.paneSwitching.enable,
    focusEnabled: caps.focus,
    rearrangeEnabled: caps.rearrange,
    sizingEnabled: caps.paneTitleBarControls.sizing,
    acquireSpaceEnabled: caps.paneTitleBarControls.acquireSpace,
    collapseEnabled: caps.paneTitleBarControls.collapse,
    resizeEnabled: caps.resize !== "none",
    layoutEnabled: caps.masterLayout,
    groupingEnabled: caps.grouping.enable,
    workspacesEnabled: caps.workspaces.enable,
  };
}

function MaximizeButton(props: {
  handle: React.RefObject<TilingCommandHandle | null>;
  interaction?: TilingInteractionCapabilities;
}) {
  const command: TilingCommand = { kind: "toggle-maximize" };
  if (!isCommandEnabled(command, gatesFor(props.interaction))) {
    return null; // maximize is disabled — don't render a dead control
  }
  return <button onClick={() => props.handle.current?.dispatch(command)}>Maximize</button>;
}

isMultiSelectModifierActive()

function

true when the event carries the multi-select modifier — Alt/Opt (altKey), the single chord both the header toggle and the Alt+G group key key off.

Signature:

declare function isMultiSelectModifierActive(event: MultiSelectModifierState): boolean;

Parameters

Parameter

Type

Description

event

MultiSelectModifierState

Returns:

boolean

Example

In a custom renderTile, add the clicked pane to the multi-selection only when the platform multi-select modifier is held (a plain click falls through to focus):

import { isMultiSelectModifierActive } from "@n-uf/hypr-tiling";

<header
  onClick={(event) => {
    if (args.isMultiSelectGroupingEnabled && isMultiSelectModifierActive(event)) {
      event.preventDefault();
      args.onToggleMultiSelect();
    }
  }}
/>

isWorkspaceNavigationCommand()

function

Whether command is a workspace-set kind (switch-workspace, cycle-workspace, move-leaf-to-workspace, reveal-tile, reset-workspace, reset-workspaces). Used by the set-mode wrapper to intercept those kinds before they reach the single-layout router.

Signature:

declare function isWorkspaceNavigationCommand(command: TilingCommand): command is Extract<TilingCommand, {
    kind: "switch-workspace" | "cycle-workspace" | "move-leaf-to-workspace" | "reveal-tile" | "reset-workspace" | "reset-workspaces";
}>;

Parameters

Parameter

Type

Description

command

TilingCommand

Returns:

command is Extract<TilingCommand, { kind: "switch-workspace" | "cycle-workspace" | "move-leaf-to-workspace" | "reveal-tile" | "reset-workspace" | "reset-workspaces"; }>

moveLeafToWorkspace()

function

Move a leaf to workspace to at placement: it leaves EVERY workspace that shows it and is seated once in to (the drag-onto-a-tab commit; DashAI moveItem). The node payload (sizing, collapsed, minBBoxPx) travels from the active workspace's seat when it has one, else the first seat in tab order; the collapse pin is re-asserted on the destination axis by insertLeafInto. When to is the leaf's only workspace this is a same-tree re-seat (extract + insert). Unchanged when the leaf or to is unknown. A source tree whose only leaf moved out becomes null.

Signature:

declare function moveLeafToWorkspace(set: TilingWorkspaceSet, leafId: string, to: TilingWorkspaceId, placement?: TilingWorkspacePlacement): TilingWorkspaceSet;

Parameters

Parameter

Type

Description

set

TilingWorkspaceSet

leafId

string

to

TilingWorkspaceId

placement

TilingWorkspacePlacement

(Optional)

Returns:

TilingWorkspaceSet

moveTileToWorkspace()

function

Move a tile to workspace to at placement: it leaves EVERY workspace that shows it and is seated once in to. Resolves the leaf through queryWorkspaceSet().workspacesOfTile (one leaf id ↔ one tile id). When the tile is seated nowhere it is seated only if options.leaf.id is given (a fresh { kind: "leaf", id, tileId }); otherwise the set is unchanged. Unchanged when to is unknown, the minted leaf id is empty or already used, or the tile is unknown and no mint id is given.

Signature:

declare function moveTileToWorkspace(set: TilingWorkspaceSet, tileId: string, to: TilingWorkspaceId, placement?: TilingWorkspacePlacement, options?: MoveTileToWorkspaceOptions): TilingWorkspaceSet;

Parameters

Parameter

Type

Description

set

TilingWorkspaceSet

tileId

string

to

TilingWorkspaceId

placement

TilingWorkspacePlacement

(Optional)

options

MoveTileToWorkspaceOptions

(Optional)

Returns:

TilingWorkspaceSet

MoveTileToWorkspaceOptions

interface

Options for moveTileToWorkspace(). The engine never invents leaf ids: a tile seated nowhere is seated only when leaf.id is given.

Signature:

interface MoveTileToWorkspaceOptions 

Properties

Property

Modifiers

Type

Description

leaf?

readonly

{ readonly id: string; }

(Optional) Leaf id to mint when tileId is seated nowhere. Ignored when the tile already has a seat (the existing binding wins). Empty / already-used ids are refused (the set is returned unchanged).

MultiSelectModifierState

interface

The modifier-key subset of a pointer/mouse event that discriminates a multi-select click from a plain click. The multi-select / grouping modifier is unified on Alt/Opt across every surface (header click + the Alt+G key), chosen for minimal cross-browser interference: altKey = Opt on macOS, Alt on Windows/Linux.

Signature:

interface MultiSelectModifierState 

Properties

Property

Modifiers

Type

Description

altKey

readonly

boolean

Whether Alt/Opt was held (the multi-select discriminator).

normaliseWheelDelta()

function

deltaX / deltaY in CSS px whatever the event's deltaMode.

Signature:

declare function normaliseWheelDelta(event: WheelEvent, clientWidth: number): {
    dx: number;
    dy: number;
};

Parameters

Parameter

Type

Description

event

WheelEvent

clientWidth

number

Returns:

{ dx: number; dy: number; }

normalizeWorkspaceName()

function

Trim a workspace name and validate it against TILING_WORKSPACE_NAME_MAX_CHARS. null when the trimmed name is empty or too long — the ops refuse (return the set unchanged) on null.

Signature:

declare function normalizeWorkspaceName(name: string): string | null;

Parameters

Parameter

Type

Description

name

string

Returns:

string | null

PersistedTilingLayout

interface

Persisted-layout adapter returned by createPersistedTilingLayout(). Every method returns the tree the host should render.

Signature:

interface PersistedTilingLayout 

Methods

Method

Description

commit(layout)

Validate + heal layout, persist the healed tree, and return it. Falls back to PersistedTilingLayout.reset() when layout (or its healed form) fails hard integrity. Call this from onLayoutChange.

load()

Read + validate + heal the persisted tree. Returns the healed tree on a clean hit, fallbackLayout when nothing is stored (or on a read/parse error), and a freshly-reset tree when the stored value fails hard integrity.

reset()

Persist and return the fallbackLayout (clears a corrupt saved tree).

removeTile()

function

Remove every seat of tileId. Unchanged when the tile is seated nowhere.

Signature:

declare function removeTile(set: TilingWorkspaceSet, tileId: string): TilingWorkspaceSet;

Parameters

Parameter

Type

Description

set

TilingWorkspaceSet

tileId

string

Returns:

TilingWorkspaceSet

renameWorkspace()

function

Rename a workspace. Unchanged when the id is unknown, the name is invalid, or already equal.

Signature:

declare function renameWorkspace(set: TilingWorkspaceSet, id: TilingWorkspaceId, name: string): TilingWorkspaceSet;

Parameters

Parameter

Type

Description

set

TilingWorkspaceSet

id

TilingWorkspaceId

name

string

Returns:

TilingWorkspaceSet

repairWorkspaceSet()

function

Deterministically repair a set so workspaceSetIssues() (with the same options) is empty afterwards. Rules run in the order of TilingWorkspaceSetRepairReason; every fix is reported. A sound set is returned by reference with no reasons. Pure.

Signature:

declare function repairWorkspaceSet(set: TilingWorkspaceSet, options?: RepairWorkspaceSetOptions): TilingWorkspaceSetRepairResult;

Parameters

Parameter

Type

Description

set

TilingWorkspaceSet

options

RepairWorkspaceSetOptions

(Optional)

Returns:

TilingWorkspaceSetRepairResult

RepairWorkspaceSetOptions

interface

Options for repairWorkspaceSet().

Signature:

interface RepairWorkspaceSetOptions extends WorkspaceSetIntegrityOptions 

Extends: WorkspaceSetIntegrityOptions

Properties

Property

Modifiers

Type

Description

mintLeafId?

readonly

(tileId: string) => string

(Optional) Leaf id for a tile the repair has to seat from scratch (an orphan with no seat anywhere). Default `` leaf-${tileId} `` (the buildDefaultDwindleLayout convention); a host with its own scheme (DashAI leaf:<itemId>) passes it so repaired seats match the ids it mints.

orphanPlacement?

readonly

TilingWorkspacePlacement

(Optional) Placement for seated orphans. Default TILING_DEFAULT_WORKSPACE_PLACEMENT.

orphanWorkspaceId?

readonly

TilingWorkspaceId

(Optional) Where orphan expected tiles are seated. Default: the active workspace.

resetWorkspaceLayout()

function

Replace ONE workspace's layout and name from the workspace of the same id in defaults. activeId is unchanged. When defaults has no such id, the workspace's layout becomes null and its name is kept. Unchanged (same reference) when the id is unknown or the workspace already matches.

Signature:

declare function resetWorkspaceLayout(set: TilingWorkspaceSet, defaults: TilingWorkspaceSet, workspaceId?: string): TilingWorkspaceSet;

Parameters

Parameter

Type

Description

set

TilingWorkspaceSet

defaults

TilingWorkspaceSet

workspaceId

string

(Optional)

Returns:

TilingWorkspaceSet

resetWorkspaceSet()

function

Replace the whole set with defaults, keeping set.activeId when that id exists in defaults (otherwise defaults.activeId). Same reference when the result is already structurally equal to set.

Signature:

declare function resetWorkspaceSet(set: TilingWorkspaceSet, defaults: TilingWorkspaceSet): TilingWorkspaceSet;

Parameters

Parameter

Type

Description

set

TilingWorkspaceSet

defaults

TilingWorkspaceSet

Returns:

TilingWorkspaceSet

resolveDragChrome()

function

Resolve a theme's drag-state chrome: every theme.dragChrome token a theme omits is filled from the PANE-SHELL-INHERITING default. The default drags with the theme's own resting chrome — the seat wears paneShell.surface (same radius / border / background / shadow as the at-rest pane), the seat frame reuses resolveFocusFrame, the ghost wrapper carries only a small neutral elevation + opacity delta, and no drop-target highlight is painted (focus follows the dragged pane). A theme that passes no dragChrome at all therefore never shows a look it did not author. Pure + referentially cheap (no memo needed; callers may still useMemo on theme). Tokens are Tailwind-JIT literals, not portal-scoped; a host that paints the ghost with inherited CSS variables must still pass TilingRendererProps.overlayPortalContainer.

Signature:

declare function resolveDragChrome(theme: TilingTheme): TilingThemeDragChromeTokens;

Parameters

Parameter

Type

Description

theme

TilingTheme

Returns:

TilingThemeDragChromeTokens

ResolvedTilingDragRecoveryCapability

interface

Fully-resolved drag-recovery configuration (no optional fields).

Signature:

interface ResolvedTilingDragRecoveryCapability 

Properties

Property

Modifiers

Type

Description

enable

boolean

Resolved self-healing recovery enable flag.

frameDeadlineMs

number

Resolved rAF-fallback slack (ms).

maxDraggingIdleMs

number

Resolved idle-watchdog deadline (ms).

transitionSlackMs

number

Resolved transition-completion slack (ms).

ResolvedTilingDropHitZoneGeometryCapability

interface

Fully-resolved drop hit-zone geometry (no optional fields). centerRatio is retained as the symmetric/representative value (equals centerRatioX when no per-axis override diverges them) for telemetry + the single-knob display; centerRatioX / centerRatioY are the per-axis values the resolver actually consumes.

Signature:

interface ResolvedTilingDropHitZoneGeometryCapability 

Properties

Property

Modifiers

Type

Description

centerMinPx

number

Resolved floor (CSS px) for the center rectangle extent.

centerRatio

number

Resolved symmetric/representative center swap-zone fraction.

centerRatioX

number

Resolved per-axis HORIZONTAL swap-zone fraction the resolver consumes.

centerRatioY

number

Resolved per-axis VERTICAL swap-zone fraction the resolver consumes.

hysteresisPx

number

Resolved boundary stickiness (pane-local CSS px).

ResolvedTilingGroupingCapability

interface

Resolved group / tabbed-stacking capability (no optional fields).

Signature:

interface ResolvedTilingGroupingCapability 

Properties

Property

Modifiers

Type

Description

enable

boolean

Whether group / tabbed-stacking is enabled.

groupTabStrip

ResolvedTilingGroupTabStripOptions

Resolved built-in strip options (present even when the strip is hidden).

showGroupTabStrip

boolean

Whether the per-group tab strip renders. Default true.

ResolvedTilingGroupTabStripOptions

interface

Fully resolved built-in strip options. renderTabLabel stays optional.

Signature:

interface ResolvedTilingGroupTabStripOptions 

Properties

Property

Modifiers

Type

Description

height

number

Strip height in CSS pixels.

placement

"top" | "bottom"

"top" above the pane header, "bottom" under the pane body.

renderTabLabel?

(member: TilingGroupTabMember) => React.ReactNode

(Optional) Custom tab label, when the host passed one.

showEject

boolean

Whether the eject control renders.

showUngroup

boolean

Whether the ungroup control renders.

theme

ResolvedTilingGroupTabStripTheme

Resolved CSS-value tokens.

ResolvedTilingGroupTabStripTheme

interface

Fully resolved TilingGroupTabStripTheme (every token set).

Signature:

interface ResolvedTilingGroupTabStripTheme 

Properties

Property

Modifiers

Type

Description

accent

string

Keyboard :focus-visible outline color.

background

string

Strip row background.

borderColor

string

Strip row hairline border color.

controlColor

string

Eject / ungroup control color.

controlHoverColor

string

Eject / ungroup control color on hover.

fontFamily

string

Tab label font family.

fontSize

string

Tab label font size (CSS length).

gap

string

Gap between pills inside the rail (CSS length).

letterSpacing

string

Tab label letter spacing (CSS length).

nestedTabActiveBackground

string

Active pill background when the group strip is the nested tier.

nestedTabActiveColor

string

Active pill label color when the group strip is the nested tier.

paddingX

string

Horizontal padding of the strip row (CSS length).

radius

string

Tab (pill) corner radius (CSS length). Rail uses radius + 2.

railBackground

string

Rounded rail background behind the pills.

railBorderColor

string

Rounded rail 1px border color.

tabActiveBackground

string

Active tab background (standalone / pane strip).

tabActiveColor

string

Active tab label color (standalone / pane strip).

tabBackground

string

Inactive tab background.

tabColor

string

Inactive tab label color.

tabHoverColor

string

Inactive tab label color on hover.

ResolvedTilingInteractionCapabilities

interface

Fully-resolved interaction capabilities (no optional fields). The resolved shape is a structural superset of TilingInteractionCapabilities, so a resolved object can be re-fed to resolveInteractionCapabilities (idempotent) and passed straight back into the interaction prop.

Signature:

interface ResolvedTilingInteractionCapabilities 

Properties

Property

Modifiers

Type

Description

coherentTransit

boolean

Whether coherent non-intersecting transit is enabled.

customCursor

boolean

Whether the custom-rendered drag cursor is used during live drag.

dragMode

TilingDragMode

Resolved drag feedback mode ("preview" vs "live").

dragRecovery

ResolvedTilingDragRecoveryCapability

Resolved drag/transition self-healing recovery configuration.

dropHitZoneGeometry

ResolvedTilingDropHitZoneGeometryCapability

Resolved adjustable drop hit-zone geometry.

focus

boolean

Whether pane focus selection is enabled.

ghostPickupScalePercent

number

Resolved ghost pickup scale as a percent of the source pane bbox.

grouping

ResolvedTilingGroupingCapability

Resolved group / tabbed-stacking capability.

keyBindings

ResolvedTilingKeyBindings

Resolved consumer key bindings (the raw registry passed through; an empty binding list + replaceDefaults: false when omitted). The renderer matches these before the default keymap path.

keymap

ResolvedTilingKeymap

Resolved keymap (every chord explicit).

masterLayout

boolean

Whether the master/stack layout engine is enabled.

maximize

ResolvedTilingMaximizeCapability

Resolved maximize-to-viewport capability.

paneSwitching

ResolvedTilingPaneSwitchingCapability

Resolved tab-like pane-switching capability.

paneTitleBarControls

ResolvedTilingPaneTitleBarControlsCapability

Resolved per-pane title-bar controls capability.

rearrange

boolean

Whether drag-to-rearrange is enabled.

resize

TilingResizeCapability

Resolved divider resize capability.

resizeHandlesVisible

boolean

Whether split-divider resize handles are visibly rendered.

slotCommitment

ResolvedTilingSlotCommitmentCapability

Resolved live-drag slot-commitment configuration.

slotHopInEnabled

boolean

Whether the single ghost hops into and fills the resolved slot.

touchDrag

ResolvedTilingTouchDragCapability

Resolved touch-drag hardening configuration.

workspaces

ResolvedTilingWorkspacesCapability

Resolved workspace-set navigation capability.

ResolvedTilingKeyBindings

interface

Fully-resolved key-binding registry (no optional fields).

Signature:

interface ResolvedTilingKeyBindings 

Properties

Property

Modifiers

Type

Description

bindings

ReadonlyArray<TilingKeyBinding>

Resolved consumer chord→command bindings (empty when none supplied).

replaceDefaults

boolean

Whether the default keymap bindings are suppressed.

ResolvedTilingKeyChord

interface

Fully-resolved key chord (every modifier explicit). Matches KeyboardEvent.code.

Signature:

interface ResolvedTilingKeyChord 

Properties

Property

Modifiers

Type

Description

alt

boolean

Whether the Alt key must be held.

code

string

Physical KeyboardEvent.code this chord matches.

ctrl

boolean

Whether the Ctrl key must be held.

meta

boolean

Whether the Meta (Cmd / Win) key must be held.

shift

boolean

Whether the Shift key must be held.

ResolvedTilingKeyChordModifiers

interface

Fully-resolved jump-to-pane modifier set.

Signature:

interface ResolvedTilingKeyChordModifiers 

Properties

Property

Modifiers

Type

Description

alt

boolean

Whether the Alt key must be held.

ctrl

boolean

Whether the Ctrl key must be held.

meta

boolean

Whether the Meta (Cmd / Win) key must be held.

shift

boolean

Whether the Shift key must be held.

ResolvedTilingKeymap

interface

Fully-resolved keymap (no optional fields).

Signature:

interface ResolvedTilingKeymap 

Properties

Property

Modifiers

Type

Description

cycleLayoutMode

ResolvedTilingKeyChord

Resolved chord to toggle the focused subtree dwindle ⇄ master.

cycleMasterOrientation

ResolvedTilingKeyChord

Resolved chord to cycle the master-area orientation.

decrementMasterCount

ResolvedTilingKeyChord

Resolved chord to remove one tile from the master area.

decrementMasterRatio

ResolvedTilingKeyChord

Resolved chord to shrink the master-area fraction.

enterMoveMode

ResolvedTilingKeyChord

Resolved chord to enter keyboard move mode.

focusCurrentOrLast

ResolvedTilingKeyChord

Resolved chord for the MRU focus current-or-last toggle.

focusDown

ResolvedTilingKeyChord

Resolved chord to move focus to the neighbor BELOW.

focusLeft

ResolvedTilingKeyChord

Resolved chord to move focus to the LEFT neighbor.

focusRight

ResolvedTilingKeyChord

Resolved chord to move focus to the RIGHT neighbor.

focusUp

ResolvedTilingKeyChord

Resolved chord to move focus to the neighbor ABOVE.

groupTabNext

ResolvedTilingKeyChord

Resolved chord to activate the next member tab of the focused group.

groupTabPrevious

ResolvedTilingKeyChord

Resolved chord to activate the previous member tab of the focused group.

incrementMasterCount

ResolvedTilingKeyChord

Resolved chord to add one tile to the master area.

incrementMasterRatio

ResolvedTilingKeyChord

Resolved chord to grow the master-area fraction.

jumpToPane

ResolvedTilingKeyChordModifiers

Resolved modifier set for the Alt+1..Alt+9 jump family.

nextPane

ResolvedTilingKeyChord

Resolved chord to cycle to the next pane.

previousPane

ResolvedTilingKeyChord

Resolved chord to cycle to the previous pane.

restore

ResolvedTilingKeyChord

Resolved chord to restore from maximize.

toggleGroup

ResolvedTilingKeyChord

Resolved chord to group/ungroup the focused pane.

toggleMaximize

ResolvedTilingKeyChord

Resolved chord to toggle maximize on the focused pane.

ResolvedTilingMaximizeCapability

interface

Resolved maximize capability (no optional fields).

Signature:

interface ResolvedTilingMaximizeCapability 

Properties

Property

Modifiers

Type

Description

enable

boolean

Whether the maximize/restore control + shortcuts are live.

keepGroupTabStrip

boolean

Whether a maximized group member renders inside its group (strip stays visible) instead of as a bare leaf.

ResolvedTilingPaneSwitchingCapability

interface

Resolved pane-switching capability (no optional fields).

Signature:

interface ResolvedTilingPaneSwitchingCapability 

Properties

Property

Modifiers

Type

Description

enable

boolean

Whether pane switching (tab strip + cycle/jump shortcuts) is live.

multiSelectGrouping

boolean

Whether Alt/Opt+click header multi-selection grouping is live.

showContentToggle

boolean

Whether the tab strip's pane-content visibility checkbox renders.

showSwitcherOverlay

boolean

Whether the Cmd+Tab-style switcher overlay renders while cycling.

showTabStrip

boolean | "maximized"

Whether the tab strip renders: always, never, or only while maximized.

tabDoubleClickMaximize

boolean

Whether double-clicking a tab toggles that pane's maximize state.

tabStrip

ResolvedTilingPaneTabStripOptions

Resolved built-in pane tab strip options.

ResolvedTilingPaneTabStripOptions

interface

Fully resolved pane tab strip options. renderTabLabel stays optional.

Signature:

interface ResolvedTilingPaneTabStripOptions 

Properties

Property

Modifiers

Type

Description

height

number

Strip height in CSS pixels.

placement

"top" | "bottom"

"top" above the viewport, "bottom" under the viewport.

renderTabLabel?

(tab: TilingPaneTab) => React.ReactNode

(Optional) Custom tab label, when the host passed one.

theme

ResolvedTilingGroupTabStripTheme

Resolved CSS-value tokens (same vocabulary as the group strip).

ResolvedTilingPaneTitleBarControlsCapability

interface

Resolved per-pane title-bar control capability (no optional fields).

Signature:

interface ResolvedTilingPaneTitleBarControlsCapability 

Properties

Property

Modifiers

Type

Description

acquireSpace

boolean

Whether the per-pane directional acquire-space controls render.

collapse

boolean

Whether the per-pane collapse-to-titlebar control + collapse commands are live.

sizing

boolean

Whether the per-pane FLEX / STATIC H / STATIC W / BOTH sizing control renders.

ResolvedTilingSlotCommitmentCapability

interface

Fully-resolved slot-commitment configuration (no optional fields).

Signature:

interface ResolvedTilingSlotCommitmentCapability 

Properties

Property

Modifiers

Type

Description

mode

TilingSlotCommitmentMode

Resolved re-resolution policy after the ghost seats in a slot.

reresolveDeltaPx

number

Resolved delta-responsive re-aim threshold in CSS px.

ResolvedTilingTouchDragCapability

interface

Fully-resolved touch-drag configuration (no optional fields).

Signature:

interface ResolvedTilingTouchDragCapability 

Properties

Property

Modifiers

Type

Description

enable

boolean

Resolved touch-drag enable flag.

longPressMs

number

Resolved long-press delay (ms) before a held touch becomes a drag.

ResolvedTilingWorkspacesCapability

interface

Resolved workspace-set navigation capability (no optional fields).

Signature:

interface ResolvedTilingWorkspacesCapability 

Properties

Property

Modifiers

Type

Description

enable

boolean

Whether workspace commands are enabled (still gated off in single-layout mode).

followMovedLeaf

boolean

Default follow for move-leaf-to-workspace when the command omits it.

springLoad

TilingSpringLoadConfig | null

Spring-loaded tab drop config, or null when disabled (the default).

switch

ResolvedTilingWorkspaceSwitchCapability

Gesture switching (both inputs default off).

resolveInteractionCapabilities()

function

Single defaulting helper for TilingInteractionCapabilities. undefined / null → all enabled; a partial override merges field-by-field over the all-enabled defaults. Uses nullish coalescing so an explicit false (e.g. rearrange: false, maximize.enable: false) is preserved and never overridden by the default. Idempotent: re-resolving a resolved object yields the same result.

Signature:

declare function resolveInteractionCapabilities(capabilities?: TilingInteractionCapabilities | null): ResolvedTilingInteractionCapabilities;

Parameters

Parameter

Type

Description

capabilities

TilingInteractionCapabilities | null

(Optional)

Returns:

ResolvedTilingInteractionCapabilities

resolveJumpedPaneId()

function

Resolve the jump-to-N leaf id (1-based), or null when out of range (no-op).

Signature:

declare function resolveJumpedPaneId(leafIds: ReadonlyArray<string>, paneNumber: number): string | null;

Parameters

Parameter

Type

Description

leafIds

ReadonlyArray<string>

paneNumber

number

Returns:

string | null

Example

Wire your own "jump to pane N" buttons against the leaf order from queryTilingLayout(), dispatching the built-in focus-jump command:

import { queryTilingLayout, resolveJumpedPaneId } from "@n-uf/hypr-tiling";

const { leafIds } = queryTilingLayout(layout);
leafIds.forEach((_, i) => {
  const paneNumber = i + 1;
  const targetLeafId = resolveJumpedPaneId(leafIds, paneNumber); // null → out of range
  // render a button that dispatches { kind: "focus-jump", paneNumber } when targetLeafId != null
});

resolveTransitionMode()

function

Resolve the paint mode a stage should actually run.

Reduced motion always wins ("none", instant, no clone). A degraded capture cannot slide, so a requested "slide" falls back to "fade". "none" and "fade" stay as requested when only degraded is set.

Signature:

declare function resolveTransitionMode(requested: TilingWorkspaceTransitionMode, flags: ResolveTransitionModeFlags): TilingWorkspaceTransitionMode;

Parameters

Parameter

Type

Description

requested

TilingWorkspaceTransitionMode

flags

ResolveTransitionModeFlags

Returns:

TilingWorkspaceTransitionMode

ResolveTransitionModeFlags

interface

Inputs that can override a requested paint mode.

Signature:

interface ResolveTransitionModeFlags 

Properties

Property

Modifiers

Type

Description

degraded

readonly

boolean

Capture marked the clone degraded (tainted canvas drawImage, or a canvas over the pixel budget). "slide" cannot paint a missing bitmap.

reducedMotion

readonly

boolean

prefers-reduced-motion: reduce is active.

resolveWorkspaceTabHover()

function

The workspace-tab hover for point, or null when it is over no tab. The FIRST target (in targets order) containing the point wins, so overlapping rects resolve deterministically. Pure; returns a fresh hover object.

Signature:

declare function resolveWorkspaceTabHover(targets: ReadonlyArray<TilingWorkspaceTabTarget>, point: TilingClientPoint): TilingWorkspaceTabDragHover | null;

Parameters

Parameter

Type

Description

targets

ReadonlyArray<TilingWorkspaceTabTarget>

point

TilingClientPoint

Returns:

TilingWorkspaceTabDragHover | null

resolveWorkspaceTabKey()

function

The roving-focus keyboard model of a tablist (WAI-ARIA tabs pattern, manual activation). focusedIndex is the tab holding focus, count the number of tabs. Pure; null for keys the strip does not handle so the host can let them propagate.

Signature:

declare function resolveWorkspaceTabKey(key: string, focusedIndex: number, count: number, orientation?: TilingWorkspaceTabsOrientation): TilingWorkspaceTabKeyAction | null;

Parameters

Parameter

Type

Description

key

string

focusedIndex

number

count

number

orientation

TilingWorkspaceTabsOrientation

(Optional)

Returns:

TilingWorkspaceTabKeyAction | null

resolveWorkspaceTransition()

function

Resolve a theme's workspace-switch motion tokens. Omitted fields fall back to 200 ms and the drag-hop easing so a theme that passes nothing never invents a look it did not author. Pure.

Signature:

declare function resolveWorkspaceTransition(theme: TilingTheme): TilingThemeWorkspaceTransitionTokens;

Parameters

Parameter

Type

Description

theme

TilingTheme

Returns:

TilingThemeWorkspaceTransitionTokens

revealTile()

function

Reveal a tile: pick prefer when that workspace shows it, else the first workspace in tab order that does. Switch activeId if needed (changed includes "workspace"); if the leaf is a group member that is not the group's activeMemberId, make it active (changed includes "tab"). Returns null when the tile is seated nowhere. Same set reference when changed is "none".

Signature:

declare function revealTile(set: TilingWorkspaceSet, tileId: string, prefer?: TilingWorkspaceId): TilingRevealTileResult | null;

Parameters

Parameter

Type

Description

set

TilingWorkspaceSet

tileId

string

prefer

TilingWorkspaceId

(Optional)

Returns:

TilingRevealTileResult | null

sampleTransitionEase()

function

Sample the timed settle curve at t in [0, 1]. Ease-out cubic so the JS requestAnimationFrame path reads as a snappy decel without parsing a CSS <easing-function> string.

Signature:

declare function sampleTransitionEase(t: number): number;

Parameters

Parameter

Type

Description

t

number

Returns:

number

ScrollChainAxis

type

The axis a scroll-chain query asks about.

Signature:

type ScrollChainAxis = "x" | "y";

ScrollChainDirection

type

Scroll direction along an axis: 1 towards the end (right / down), -1 towards the start.

Signature:

type ScrollChainDirection = -1 | 1;

ScrollChainPort

interface

Read-only capability: can any scroll container from element up to (and including) the viewport root still scroll along axis in direction? The FSM refuses to arm while true — the inner content owns that wheel / pan. element is opaque to the engine (the DOM adapter narrows it).

Signature:

interface ScrollChainPort<TElement = unknown> 

Methods

Method

Description

canScrollFurther(element, axis, direction)

true while some container from element to the root can still scroll along axis in direction.

setWorkspaceLayout()

function

Replace ONE workspace's tree — the in-tree commit path the renderer uses in set mode (a drag / resize / collapse edit of the active tree). Only that workspace changes; other seats are untouched (a cross-tree move is moveLeafToWorkspace()). Unchanged when the id is unknown, the tree is the same reference, or a non-null tree fails isStructurallyValidLayout.

Signature:

declare function setWorkspaceLayout(set: TilingWorkspaceSet, id: TilingWorkspaceId, layout: TilingLayoutNode | null): TilingWorkspaceSet;

Parameters

Parameter

Type

Description

set

TilingWorkspaceSet

id

TilingWorkspaceId

layout

TilingLayoutNode | null

Returns:

TilingWorkspaceSet

showInWorkspace()

function

Seat a leaf that is already in the set in ANOTHER workspace too (shown in both; DashAI showItem). The new seat is a fresh { id, tileId } node — sizing / collapsed are per workspace. Unchanged when the leaf is unknown, to is unknown, or to already shows it.

Signature:

declare function showInWorkspace(set: TilingWorkspaceSet, leafId: string, to: TilingWorkspaceId, placement?: TilingWorkspacePlacement): TilingWorkspaceSet;

Parameters

Parameter

Type

Description

set

TilingWorkspaceSet

leafId

string

to

TilingWorkspaceId

placement

TilingWorkspacePlacement

(Optional)

Returns:

TilingWorkspaceSet

showTileInWorkspace()

function

Seat a tile that is already in the set in ANOTHER workspace too (shown in both). Unchanged when the tile is unknown, to is unknown, or to already shows it. Never mints a leaf id — a tile seated nowhere stays seated nowhere.

Signature:

declare function showTileInWorkspace(set: TilingWorkspaceSet, tileId: string, to: TilingWorkspaceId, placement?: TilingWorkspacePlacement): TilingWorkspaceSet;

Parameters

Parameter

Type

Description

set

TilingWorkspaceSet

tileId

string

to

TilingWorkspaceId

placement

TilingWorkspacePlacement

(Optional)

Returns:

TilingWorkspaceSet

switchWorkspace()

function

Make id the active workspace. Unchanged when unknown or already active.

Signature:

declare function switchWorkspace(set: TilingWorkspaceSet, id: TilingWorkspaceId): TilingWorkspaceSet;

Parameters

Parameter

Type

Description

set

TilingWorkspaceSet

id

TilingWorkspaceId

Returns:

TilingWorkspaceSet

TILING_ACCENT_HUES

variable

The hue atoms for every accent. Keyed by the closed TilingTileAccent union so the compiler enforces full coverage. Theme-independent: themes choose which atoms to apply.

Signature:

TILING_ACCENT_HUES: Record<TilingTileAccent, TilingAccentHue>

TILING_DASHBOARD_PRESET

variable

Interaction preset for static (config-driven) product dashboards: only height dividers resize (resize: "vertical"), and every interactive surface that assumes a rearrangeable/maximizable lab layout is off (no drag-rearrange, no pane focus selection, no maximize, no pane switching / tab strip). Dashboards pass this instead of hand-repeating the disable set.

Per-pane title-bar controls (sizing + acquire-space) are OFF here too: dashboards ship a curated layout with PRE-CONFIGURED static panes and are resize-only by intent, so letting an end-user re-pin a pane's bbox or have one pane absorb its siblings' space would fight the authored composition. End-user per-pane sizing belongs to interactive workspaces (the all-enabled default), not config-driven dashboards.

Signature:

TILING_DASHBOARD_PRESET: TilingInteractionCapabilities

TILING_DEFAULT_COLLAPSED_EXTENT_PX

variable

Default collapsed titlebar-only extent (CSS px) when TilingLayoutConfig.collapsedExtentPx is undefined — sized to the built-in pane header height so a collapsed pane shows just its title bar.

Signature:

TILING_DEFAULT_COLLAPSED_EXTENT_PX: number

TILING_DEFAULT_WORKSPACE_PLACEMENT

variable

Default placement when a leaf enters a tree without a host placement: a new root split, leaf last.

Signature:

TILING_DEFAULT_WORKSPACE_PLACEMENT: TilingWorkspacePlacement

TILING_INTERACTION_CAPABILITY_DEFAULTS

variable

All-enabled defaults. An undefined capability config (or any undefined field) resolves to these via resolveInteractionCapabilities.

The lone opt-IN exception is paneSwitching.showContentToggle: it defaults to false. That checkbox (a group tab strip's "show pane body" toggle) is a development / demo affordance — a consumer app renders its own pane content and never wants an end-user control that blanks it. Suppressed by default, the initial pane-content-visible flag pins ON (see resolveInitialPaneContentVisible), so panes paint their content at rest with no wiring. A tooling surface that genuinely wants the toggle (e.g. a tooling surface) opts back in with paneSwitching: { showContentToggle: true }.

Signature:

TILING_INTERACTION_CAPABILITY_DEFAULTS: ResolvedTilingInteractionCapabilities

TILING_MAIN_WORKSPACE_ID

variable

Id of the single workspace workspaceSetOfLayout mints for a one-tree host.

Signature:

TILING_MAIN_WORKSPACE_ID: TilingWorkspaceId

TILING_MAIN_WORKSPACE_NAME

variable

Name of the single workspace workspaceSetOfLayout mints for a one-tree host.

Signature:

TILING_MAIN_WORKSPACE_NAME: string

TILING_PANE_TAB_STRIP_THEME_DEFAULTS

variable

Default CSS tokens for the top-level pane tab strip: same row as the group strip, with a solid active pill. Hosts override any subset via paneSwitching.tabStrip.theme.

Signature:

TILING_PANE_TAB_STRIP_THEME_DEFAULTS: ResolvedTilingGroupTabStripTheme

TILING_SPRING_LOAD_DEFAULTS

variable

Defaults of TilingSpringLoadConfig.

Signature:

TILING_SPRING_LOAD_DEFAULTS: TilingSpringLoadConfig

TILING_THEME_REGISTRY

variable

Built-in theme registry, keyed by the closed TilingThemeId union. Adding a member to TilingThemeId forces a new entry here (the Record fails to compile until filled in).

Signature:

TILING_THEME_REGISTRY: Record<TilingThemeId, TilingTheme>

TILING_TILE_ACCENT_SWATCHES

variable

Picker-ready metadata (accent + label + swatch class) for every accent.

Signature:

TILING_TILE_ACCENT_SWATCHES: readonly TilingTileAccentSwatch[]

TILING_TILE_ACCENTS

variable

Ordered, enumerable accent palette — the generic capability a consumer iterates to build an accent picker.

Signature:

TILING_TILE_ACCENTS: readonly TilingTileAccent[]

TILING_WORKSPACE_NAME_MAX_CHARS

variable

Maximum workspace name length (after trim) createWorkspace / renameWorkspace admit.

Signature:

TILING_WORKSPACE_NAME_MAX_CHARS: number

TILING_WORKSPACE_SWIPE_DEFAULTS

variable

Defaults of TilingWorkspaceSwipeConfig.

Signature:

TILING_WORKSPACE_SWIPE_DEFAULTS: TilingWorkspaceSwipeConfig

TILING_WORKSPACES_MAX

variable

Maximum number of workspaces createWorkspace admits.

Signature:

TILING_WORKSPACES_MAX: number

TilingAccentHue

interface

Per-accent HUE tokens — the raw color atoms a theme composes. Decoupled from the previous bundled accent theme so a calm theme can borrow an accent's hue (border/text/ring) WITHOUT inheriting its heavy neon glow, and a refined theme can pick the softened glow. Every field is a literal Tailwind class so the JIT emits it.

Signature:

interface TilingAccentHue 

Properties

Property

Modifiers

Type

Description

focusBorder

readonly

string

Focused-pane border color.

focusGlow

readonly

string

Full-intensity neon focus glow (box-shadow).

focusGlowSoft

readonly

string

Dialed-back focus glow for refined / calm themes (box-shadow).

focusRing

readonly

string

Focused-pane focus-ring color.

label

readonly

string

Human-facing palette label (also the picker swatch label).

surfaceBorder

readonly

string

Resting pane border tint (low alpha).

surfaceShadow

readonly

string

Resting pane colored drop-shadow tint.

swatch

readonly

string

Solid background for a palette swatch dot.

tabBg

readonly

string

Active tab/switcher chip translucent fill.

tabBgSoft

readonly

string

Subtle active-tab fill for low-contrast themes.

tabBorder

readonly

string

Active tab/switcher chip border.

text

readonly

string

Accent title / metadata text.

textStrong

readonly

string

Strong accent text used on active chips.

TilingChromeFocusOutline

type

How the renderer handles native user-agent focus outlines on its own chrome (pane roots article[data-leaf-id], tabs, header buttons/links inside .hpt-root). The renderer injects a stylesheet SCOPED to .hpt-root, so it never touches app chrome outside the tiling tree.

  • "suppress" (default): inject the scoped stylesheet that removes UA focus outlines (and tap-highlight) on hypr chrome. Hypr paints its own focus affordance (theme border/frame), so the raw UA :focus-visible ring — which a bare Shift/modifier keypress can promote on an already-focused pane — is redundant and visually noisy. This is the historical default. - "native": do NOT inject the suppression rules; native UA focus outlines are preserved on hypr chrome. Pick this for a fully keyboard-accessible default that relies on the browser focus ring. (Theme token classes such as outline-none on the pane surface are authored in the theme object and are controlled there, independently of this switch.)

Signature:

type TilingChromeFocusOutline = "suppress" | "native";

TilingClientPoint

interface

Window-client point (CSS px) the host reports for an external hover / drop.

Signature:

interface TilingClientPoint 

Properties

Property

Modifiers

Type

Description

x

readonly

number

Horizontal window-client coordinate (CSS px).

y

readonly

number

Vertical window-client coordinate (CSS px).

TilingClientRect

interface

A window-client rectangle (CSS px), the DOMRect subset the hit-test reads.

Signature:

interface TilingClientRect 

Properties

Property

Modifiers

Type

Description

bottom

readonly

number

Bottom edge (window-client CSS px).

left

readonly

number

Left edge (window-client CSS px).

right

readonly

number

Right edge (window-client CSS px).

top

readonly

number

Top edge (window-client CSS px).

TilingCollapseBodyMode

type

Collapsed pane body MOUNT policy (HT-COLLAPSE-BODY-MODE) — lib-controlled, not a per-consumer renderTile decision (see TilingPaneBody(), which is the ONE place this policy takes effect):

  • "keep-mounted" (default) — a collapsed pane's body stays mounted (its React subtree is preserved, only hidden via display: none), so scroll position, focus, form state, iframes, etc. inside the pane survive collapse/expand. - "unmount" — a collapsed pane's body is removed from the tree entirely (like the content-visibility toggle turned off), trading state preservation for a lighter DOM / fewer live subtrees while collapsed.

Signature:

type TilingCollapseBodyMode = "keep-mounted" | "unmount";

TilingCommandGates

interface

The capability enable-flags a command may require. Superset of TilingKeymapActionGuards (which only covers the keyboard-reachable actions) with the sizing / acquire-space / resize gates the programmatic command set also touches. The renderer builds this from its resolved capabilities.

Signature:

interface TilingCommandGates 

Properties

Property

Modifiers

Type

Description

acquireSpaceEnabled

boolean

Directional acquire-space command.

collapseEnabled

boolean

Per-pane collapse (toggle-collapse / set-collapsed) commands.

focusEnabled

boolean

Focus selection commands.

groupingEnabled

boolean

Group / tabbed-stacking commands (HT-GROUP-TABBED-STACKING).

layoutEnabled

boolean

Master/stack layout-mode commands (HT-LAYOUT-MASTER-STACK).

maximizeEnabled

boolean

Maximize / restore commands.

paneSwitchingEnabled

boolean

Pane-switching (focus-cycle / focus-jump) commands.

rearrangeEnabled

boolean

Drag/keyboard rearrange (move / swap / insert) commands.

resizeEnabled

boolean

Divider resize (set-split-ratio / toggle-split-axis) commands.

sizingEnabled

boolean

Per-pane title-bar sizing (set-sizing) command.

workspacesEnabled

boolean

Workspace-set navigation and reset commands (switch-workspace, cycle-workspace, move-leaf-to-workspace, reveal-tile, reset-workspace, reset-workspaces). The renderer sets this false in single-layout mode (no workspaces prop) so a dispatch there is a no-op, and false when interaction.workspaces.enable is false. Reset commands additionally no-op when the renderer has no workspaceDefaults seed.

TilingDeleteWorkspaceResult

interface

The result of deleteWorkspace().

Signature:

interface TilingDeleteWorkspaceResult 

Properties

Property

Modifiers

Type

Description

removedTileIds

readonly

ReadonlyArray<string>

Tile ids that were shown ONLY in the deleted workspace, in that tree's reading order — they are now seated nowhere, so a host with a tile registry reconciles (DashAI: items shown nowhere leave items). Empty when refused.

set

readonly

TilingWorkspaceSet

The set without the workspace (the input by reference when refused).

TilingDimension

type

The two layout dimensions a pane can be sized along, independent of split axis.

Signature:

type TilingDimension = "width" | "height";

TilingDragGhostMode

type

How the live-drag ghost paints while a leaf is carried.

  • "footprint" (default) — today's tile-sized ghost, origin-offset from the pickup grab point. Unchanged behaviour. - "compact" — always the small chip under the cursor. - "auto" — compact while TilingRendererCommonProps.externalDragHover is non-null, footprint otherwise. The 26.9.x forward-compatible subset of the WorkspaceSet drag scope (_agent/workspace-set-concept.md §5.5).

Signature:

type TilingDragGhostMode = "footprint" | "compact" | "auto";

TilingDragHandle()

function

The drag-pickup surface of a custom pane (typically the header). Wires the renderer's onHandlePointerDown and sets touch-action: none so a touch press starts a drag instead of scrolling, and folds in the Alt/Opt+click multi-select toggle (which must not establish focus). Renders a <div>; bring your own className / style / children.

Signature:

declare function TilingDragHandle(input: TilingDragHandleProps): React.ReactElement;

Parameters

Parameter

Type

Description

{ pane, style, ref, ...rest }

(not declared)

input

TilingDragHandleProps

Returns:

React.ReactElement

TilingDragHandleProps

interface

Props for TilingDragHandle().

Signature:

interface TilingDragHandleProps extends Omit<React.HTMLAttributes<HTMLElement>, "onPointerDown" | "onClick"> 

Extends: Omit<React.HTMLAttributes<HTMLElement>, "onPointerDown" | "onClick">

Properties

Property

Modifiers

Type

Description

pane

Pick<TilingRenderTileProps, "onHandlePointerDown" | "isMultiSelectGroupingEnabled" | "onToggleMultiSelect">

The renderTile args for this pane (the drag-pickup handler and the multi-select toggle handlers are read). Pass the whole args object.

ref?

React.Ref<HTMLDivElement>

(Optional) Forwarded to the handle element (a host group-drop target, for example).

TilingDragMode

type

Drag-to-rearrange feedback mode — how a pending move is shown WHILE dragging, before it is committed on drop.

  • "preview" — non-committing preview (default). The pending result is shown via the translucent PROJECTED landing overlays (S' / T' / successor) painted over the unchanged layout; the layout tree is NOT mutated until drop. - "live" — Hyprland-faithful detach drag. On pickup the dragged source leaf is detached and the remaining tree reflows ONCE to close the gap (removeLeafTile); that frozen provisional tree is held for the whole drag (it does NOT re-reflow as the cursor moves). The detached source follows the cursor as a ghost, and the drop is resolved + committed exactly ONCE on release against the original layout via the SAME resolver + reducers preview uses (swapLeafTiles / insertLeafAdjacent), so the live commit equals the preview commit for the same intent. The prop tree is never mutated pre-commit, so cancel / Escape / dragleave-abort / off-viewport revert is lossless.

NOTE on vocabulary: "projected" names the RENDERING TECHNIQUE for the preview overlays (projected geometry, S' / T' / successor). "preview" vs "live" is the interaction MODE. The mode is never called "projected".

Signature:

type TilingDragMode = "preview" | "live";

TilingDragRecoveryCapability

interface

Drag / transition self-healing recovery configuration. Hardens the live-drag ANIMATION + TIMING layer (not the FSM, which is already un-wedgeable) against frame starvation — background-tab requestAnimationFrame suspension, CPU throttling, long tasks, dropped/late pointer events, interrupted transitions. The recovery primitives live in drag-recovery.ts; this is their typed knob surface. All deadlines are documented multiples of the animation duration or of a frame, never symptom-tuned constants. Only meaningful in dragMode: "live".

  • enable — master gate for the recovery layer (idle watchdog + rAF-with-timeout arming + idempotent style teardown). Default true. - maxDraggingIdleMs — the drag idle-watchdog deadline: armed / dragging with no POINTER_MOVE / TARGET_RESOLVED progress for longer than this (measured MONOTONICALLY, so throttle-robust) force-reconciles to idle via the existing POINTER_CANCEL edge with pointer capture released. Default 30 × BASELINE_DRAG_HOP_DURATION_MS ≈ 5100ms. - frameDeadlineMs — the rAF-fallback slack: each FLIP "play-to-identity" write is armed as a requestAnimationFrame racing a setTimeout of this long, first-wins + idempotent, so a starved frame never leaves an element frozen at its inverted First. Default ~2 frames ≈ 32ms. - transitionSlackMs — the transition-completion slack: style cleanup fires on transitionend OR duration + transitionSlackMs, whichever first. Default 60ms (names the existing +60 mask-close slack).

Signature:

interface TilingDragRecoveryCapability 

Properties

Property

Modifiers

Type

Description

enable?

boolean

(Optional) Enable the self-healing recovery layer. Default true.

frameDeadlineMs?

number

(Optional) rAF-fallback slack (ms). Default ~2 frames ≈ 32.

maxDraggingIdleMs?

number

(Optional) Idle-watchdog deadline (ms). Default 30 × BASELINE_DRAG_HOP_DURATION_MS ≈ 5100.

transitionSlackMs?

number

(Optional) Transition-completion slack (ms). Default 60.

TilingDropAction

type

The outcome a drop resolves to: exchange tiles ("swap"), insert at an edge ("edge-insert"), insert into an existing split container ("split-container-insert"), merge into a tabbed group ("group-merge"), or no valid action ("none").

Signature:

type TilingDropAction = "swap" | "edge-insert" | "split-container-insert" | "group-merge" | "none";

TilingDropHitZoneGeometryCapability

interface

Adjustable drag-drop HIT-ZONE GEOMETRY. These knobs shape the five-region partition every pane uses to map a cursor position to a drop intent: one central SWAP rectangle plus four directional INSERT trapezoids (top / right / bottom / left) formed by the diagonals from each pane corner to the nearest center-rectangle corner (see drop-intent-resolver.ts). The partition tiles the full pane with no gaps; the same geometry drives both resolution and the visual overlay, so the drawn zone is exactly the resolved zone.

The model is SYMMETRIC: a single centerRatio sizes the center rectangle on BOTH axes, which means the directional edge band depth is fully derived from it as (1 - centerRatio) / 2 per axis (there is no independent edge-band knob, and no separate horizontal/vertical asymmetry — the diagonal-trapezoid construction uses one ratio). CORNERS are not a distinct zone: a corner resolves to whichever edge trapezoid contains it, with a deterministic tie-break order (top → right → bottom → left) on an exact diagonal.

Every field is optional; an undefined field resolves to the documented default (which equals today's TILING_DROP_INTENT_CONFIG), so omitting this object leaves drop-zone behavior exactly as it was.

Signature:

interface TilingDropHitZoneGeometryCapability 

Properties

Property

Modifiers

Type

Description

centerMinPx?

number

(Optional) Floor (CSS px) for the center rectangle's extent on each axis so tiny panes keep a usable swap target even when width * centerRatio would collapse it. Default 24.

centerRatio?

number

(Optional) Fraction of each pane axis spanned by the central SWAP rectangle. The directional INSERT edge band depth is the complement, (1 - centerRatio) / 2. Clamped to [0.05, 0.95] by the resolver. Default 0.34. Acts as the SYMMETRIC convenience: it sets BOTH axes unless a per-axis override (centerRatioX / centerRatioY) is supplied for that axis.

centerRatioX?

number

(Optional) Per-axis HORIZONTAL (width) swap-zone fraction override. When set, it sizes the center rectangle's X extent independently of centerRatio, letting a non-square pane carry an axis-specific swap-zone proportion. Falls back to centerRatio then the default 0.34. Clamped to [0.05, 0.95].

centerRatioY?

number

(Optional) Per-axis VERTICAL (height) swap-zone fraction override. When set, it sizes the center rectangle's Y extent independently of centerRatio. Falls back to centerRatio then the default 0.34. Clamped to [0.05, 0.95].

hysteresisPx?

number

(Optional) Boundary stickiness (pane-local CSS px): once the cursor is in a zone it must cross the boundary by this much before the classification switches, suppressing sub-pixel flicker at the trapezoid edges. 0 disables hysteresis. Default 6.

TilingExternalDragHover

type

Host-reported "pointer is over an external drop target" (a workspace tab, a chat panel — anything outside the tiling tree). The engine does not hit-test host chrome; the host (or useTilingWorkspaceTabs) drives this from its own over state. Discriminated on kind; a plain { targetId, point } literal is the TilingExternalDropHover variant.

Signature:

type TilingExternalDragHover = TilingExternalDropHover | TilingWorkspaceTabDragHover;

References: TilingExternalDropHover, TilingWorkspaceTabDragHover

TilingExternalDragHoverResolver

type

Engine-driven external hover resolution: called with the pointer's window- client position (once per processed drag frame and synchronously at release) while a leaf is being dragged; returns the external target under the pointer or null. Lets the renderer own the hit-test — the host's tab strip only registers rects (useTilingWorkspaceTabs supplies one).

Signature:

type TilingExternalDragHoverResolver = (point: TilingClientPoint, leafId: string) => TilingExternalDragHover | null;

References: TilingClientPoint, TilingExternalDragHover

TilingExternalDropHover

interface

Host-reported "pointer is over an external drop target" the ENGINE cannot settle itself (a chat panel, a sidebar — anything outside the tiling tree that is not a workspace tab). The host claims the leaf on release through TilingRendererCommonProps.onExternalDrop. kind may be omitted — a bare { targetId, point } literal is this variant.

Signature:

interface TilingExternalDropHover 

Properties

Property

Modifiers

Type

Description

kind?

readonly

"external"

(Optional) Discriminant; omitted on a bare literal.

point

readonly

TilingClientPoint

Pointer position over the target (window-client CSS px).

targetId

readonly

string

The host target's id (what onExternalDrop and the ghostChip slot receive).

TilingFocusDirection

type

A directional focus / move target relative to the current pane.

Signature:

type TilingFocusDirection = "left" | "right" | "up" | "down";

TilingGhostChipContext

interface

Arguments the theme ghostChip slot receives when the compact ghost is painted. targetId is present only while TilingExternalDragHover is set.

Signature:

interface TilingGhostChipContext 

Properties

Property

Modifiers

Type

Description

leafId

readonly

string

The dragged leaf.

point

readonly

TilingClientPoint

Where the chip is anchored (window-client CSS px).

targetId?

readonly

string

(Optional) The external target under the pointer, when TilingExternalDragHover is set.

title?

readonly

string

(Optional) The dragged tile's title, when it has one.

workspaceId?

readonly

string

(Optional) Present while the hover is a TilingWorkspaceTabDragHover: the destination workspace.

TilingGroupingCapability

interface

Group / tabbed-stacking capability (HT-GROUP-TABBED-STACKING), the object form of the grouping capability. A bare boolean at the grouping slot is shorthand for { enable }.

Signature:

interface TilingGroupingCapability 

Properties

Property

Modifiers

Type

Description

enable?

boolean

(Optional) Enable grouping. When true (default), the grouping commands (toggle-group, ungroup, add-to-group, group-tab-cycle, group-tab-jump, group-leaves) and their default keybindings are live, and drag-onto-a-group's-tab-strip merges into the group. When false, those commands are no-ops and drag-into-group is disabled (an existing group still renders per this capability's showGroupTabStrip).

groupTabStrip?

TilingGroupTabStripOptions

(Optional) Built-in group tab strip. Ignored when showGroupTabStrip is false. Omitted fields resolve to the defaults in groupTabStrip below.

showGroupTabStrip?

boolean

(Optional) Render the per-group tab strip (the member tabs a tabbed-stacking group paints above its active member). Default true. When false, the strip is suppressed and the group renders only its active member; the keyboard group commands (group-tab-cycle, group-tab-jump, remove-from-group, ungroup) keep working, so a consumer that hides the strip can paint its own group chrome and route it through the same commands. Note: drag-onto-the-strip group merge requires a mounted strip, so hiding the strip also removes that drop target. Distinct from paneSwitching.showTabStrip, which governs the TOP-LEVEL tab strip only.

TilingGroupMemberView

interface

One member of a tabbed group, as seen by a custom renderTile through TilingRenderTileGroupContext.members. Mirrors what the built-in group tab strip paints per tab: the member's identity, its resolved tile payload, its 1-based position (the group-tab-jump operand — also the keyboard Alt+<n> label), and whether it is the active (rendered) member.

Signature:

interface TilingGroupMemberView 

Properties

Property

Modifiers

Type

Description

isActive

readonly

boolean

Whether this member is the group's active (rendered) member.

leafId

readonly

string

The member's leaf node id.

memberNumber

readonly

number

1-based position in the group's member order — the operand TilingRenderTileGroupContext.activateMember takes (and the number the built-in strip / keyboard Alt+<n> jump display).

tile

readonly

TilingTile | null

The resolved tile payload, or null when the tile map has no match.

tileId

readonly

string

The tile id the member's leaf points at.

TilingGroupTabMember

interface

One member of a built-in group tab strip, as seen by TilingGroupTabStripOptions.renderTabLabel.

Signature:

interface TilingGroupTabMember 

Properties

Property

Modifiers

Type

Description

active

boolean

Whether this member is the group's active member.

id

string

Group-member leaf id.

tileId

string

Tile id the member displays.

title

string

Tile title, or the tile id when the tile has no title.

TilingGroupTabStripOptions

interface

Built-in group tab strip options. The strip is the canonical group chrome: hosts theme it instead of painting their own tabs.

Signature:

interface TilingGroupTabStripOptions 

Properties

Property

Modifiers

Type

Description

height?

number

(Optional) Strip height in CSS pixels. Default 28.

placement?

"top" | "bottom"

(Optional) "top" (default) paints the strip above the pane header. "bottom" paints the same strip under the pane body.

renderTabLabel?

(member: TilingGroupTabMember) => React.ReactNode

(Optional) Custom tab label. Default is the tile title. The string title is always the tab's title attribute (ellipsis tooltip).

showEject?

boolean

(Optional) Show the eject control at the right end. Ejects the active member out of the group (remove-from-group). Default true.

showUngroup?

boolean

(Optional) Show the ungroup control at the right end. Default true.

theme?

TilingGroupTabStripTheme

(Optional) CSS-value tokens. Omitted tokens keep the library default.

TilingGroupTabStripTheme

interface

CSS-value tokens for the built-in group tab strip. Every field is optional; omitted tokens resolve to the library default (dark row, quiet active pill). Applied as CSS custom properties on the strip element.

Signature:

interface TilingGroupTabStripTheme 

Properties

Property

Modifiers

Type

Description

accent?

string

(Optional) Keyboard :focus-visible outline color.

background?

string

(Optional) Strip row background.

borderColor?

string

(Optional) Strip row hairline border color.

controlColor?

string

(Optional) Eject / ungroup control color.

controlHoverColor?

string

(Optional) Eject / ungroup control color on hover.

fontFamily?

string

(Optional) Tab label font family.

fontSize?

string

(Optional) Tab label font size (CSS length).

gap?

string

(Optional) Gap between pills inside the rail (CSS length).

letterSpacing?

string

(Optional) Tab label letter spacing (CSS length).

nestedTabActiveBackground?

string

(Optional) Active pill background when the group strip is the nested tier.

nestedTabActiveColor?

string

(Optional) Active pill label color when the group strip is the nested tier. Omitted → tabActiveColor.

paddingX?

string

(Optional) Horizontal padding of the strip row (CSS length).

radius?

string

(Optional) Tab (pill) corner radius (CSS length). Rail uses radius + 2.

railBackground?

string

(Optional) Rounded rail background behind the pills.

railBorderColor?

string

(Optional) Rounded rail 1px border color. Omitted → borderColor.

tabActiveBackground?

string

(Optional) Active tab background (standalone / pane strip).

tabActiveColor?

string

(Optional) Active tab label color (standalone / pane strip).

tabBackground?

string

(Optional) Inactive tab background.

tabColor?

string

(Optional) Inactive tab label color.

tabHoverColor?

string

(Optional) Inactive tab label color on hover.

TilingHideTileResult

interface

The result of hideTileFromWorkspace().

Signature:

interface TilingHideTileResult 

Properties

Property

Modifiers

Type

Description

orphaned

readonly

boolean

true when the tile now has no seat in any workspace.

set

readonly

TilingWorkspaceSet

The set without that seat (the input by reference when refused).

TilingInteractionCapabilities

interface

Public, reactive interaction-capability flags for the tiling renderer. All fields are optional; an undefined field (or an undefined config object) resolves to the all-enabled default via resolveInteractionCapabilities.

Signature:

interface TilingInteractionCapabilities 

Properties

Property

Modifiers

Type

Description

coherentTransit?

boolean

(Optional) Coherent non-intersecting transit. When true (default), the moving source ghost and the displaced target never visually OVERLAP mid-transition: on a SWAP both moving boxes scale down toward ~70% mid-transit (best-effort) then scale back into place, coordinated with the survivor reflow so even if their paths cross the shrunk boxes do not collide. Trivial / no-op for edge-insert (the boxes never trade places). Only meaningful in dragMode: "live"; skipped under prefers-reduced-motion.

customCursor?

boolean

(Optional) Custom-rendered drag cursor (interaction tier "c"). When true (default), an active live drag hides the OS cursor (cursor: none on the tiling root) and renders a transform-pinned cursor element that follows the pointer and reflects the drag FSM + drop validity (neutral grab / directional arrow / swap / blocked). When false, the native OS cursor is used during drag. Only meaningful in dragMode: "live".

dragMode?

TilingDragMode

(Optional) Drag-to-rearrange feedback mode: "preview" (non-committing projected overlays) vs "live" (Hyprland detach: source detached on pickup, frozen tree, cursor-following ghost, commit on release). Default "live" (the resolved default in TILING_INTERACTION_CAPABILITY_DEFAULTS). Only meaningful when rearrange is enabled; live mode is also unreachable for static-pane layouts (drag-rearrange is auto-gated off there).

dragRecovery?

TilingDragRecoveryCapability

(Optional) Drag / transition self-healing recovery (idle watchdog + rAF-with-timeout animation arming + idempotent transient-style teardown). Hardens the live-drag animation/timing layer against frame starvation. Only meaningful in dragMode: "live". Default all-resolved (enable: true, maxDraggingIdleMs ≈ 5100, frameDeadlineMs ≈ 32, transitionSlackMs: 60).

dropHitZoneGeometry?

TilingDropHitZoneGeometryCapability

(Optional) Adjustable drag-drop hit-zone geometry (center swap fraction, center floor, boundary hysteresis). Undefined → today's TILING_DROP_INTENT_CONFIG defaults. Reactive: changing it at runtime re-shapes the drop zones (and their visual overlay) immediately.

focus?

boolean

(Optional) Pane focus selection. When false, focus selection is suppressed. Default true.

ghostPickupScalePercent?

number

(Optional) Ghost pickup scale as a percent of the source pane's full bbox. On drag start the live-mode ghost animates from the source pane's full bbox to this fraction of it; values <100% read as lifted/shrunk. The free-following ghost rests at this scale and morphs toward the resolved slot's bbox on hop-in. Range [10, 150]; clamped where consumed. Default 90. Only meaningful in dragMode: "live"; skipped under prefers-reduced-motion.

grouping?

boolean | TilingGroupingCapability

(Optional) Group / tabbed-stacking (HT-GROUP-TABBED-STACKING). A bare boolean is shorthand for { enable }; the object form additionally governs the per-group tab strip via showGroupTabStrip (see TilingGroupingCapability). Default true (grouping enabled, strip rendered).

keyBindings?

TilingKeyBindings

(Optional) Public chord→command binding registry. Consumer bindings augment (and on a chord collision override) the default keymap bindings; set replaceDefaults to drop the built-in keymap path entirely. Undefined → the default keymap bindings only.

keymap?

TilingKeymap

(Optional) Top-level keymap. Capability-level keymap overrides take precedence over this; this takes precedence over the documented defaults.

masterLayout?

boolean

(Optional) Master/stack layout engine (HT-LAYOUT-MASTER-STACK). When true (default), the layout-mode + master commands (cycle-layout-mode, adjust-master-count, cycle-master-orientation, adjust-master-ratio, …) and their default keybindings are live, and a subtree set to layoutMode: "master" renders as a master area + stack. When false, those commands are no-ops (a master-mode tree still renders via the resolver, but the operator cannot change the mode/params). Default true.

maximize?

TilingMaximizeCapability

(Optional) Maximize-to-viewport capability. Default all-enabled.

paneSwitching?

TilingPaneSwitchingCapability

(Optional) Tab-like pane-switching capability. Default all-enabled.

paneTitleBarControls?

TilingPaneTitleBarControlsCapability

(Optional) Per-pane title-bar controls (in-header sizing + acquire-space). Default all-enabled. Reactive: toggling a flag at runtime shows/hides the controls without remount.

rearrange?

boolean

(Optional) Drag-to-rearrange (move / swap / edge-insert) capability. When false, panes are not draggable and no drop overlays / hit-zones activate. Default true.

resize?

TilingResizeCapability

(Optional) Divider resize capability. Default "both" (every divider resizable).

resizeHandlesVisible?

boolean

(Optional) Whether split-divider resize handles are visibly rendered. false hides handle chrome (separator paint / hover affordance) while preserving the divider hit-target and resize capability gating from resize. Default true (chrome on when resize is enabled; opt out for gap-only).

slotCommitment?

TilingSlotCommitmentCapability

(Optional) Live-drag slot re-resolution / commitment policy (the single ghost hops INTO and FILLS the resolved slot). Only meaningful in dragMode: "live". Default all-resolved (mode: "delta-responsive", reresolveDeltaPx: 24).

slotHopInEnabled?

boolean

(Optional) Live-drag slot hop-in. Selects between the two drag-presentation behaviors for the dragged pane:

  • true (default, ORIGINAL): the single content-honoring ghost HOPS INTO and FILLS the resolved slot — the seat rect is measured so the ghost seats as the single instance and no separate empty reservation lingers beside a free-following ghost. - false (the seat-measurement is deliberately skipped): the ghost free-follows the cursor and the in-tree content-less reservation slot stays shown — the reservation-plus-ghost duality.

Orthogonal to the CONTENT toggle: in BOTH modes the pane body honors content uniformly (see resolvePaneBodyRenderMode). Only meaningful in dragMode: "live". Default true.

touchDrag?

TilingTouchDragCapability

(Optional) Touch-drag hardening (touch enable + long-press disambiguation). Only meaningful when rearrange is enabled. Default all-resolved (enable: true, longPressMs: 220).

workspaces?

boolean | TilingWorkspacesCapability

(Optional) Workspace-set navigation and reset commands (switch-workspace, cycle-workspace, move-leaf-to-workspace, reveal-tile, reset-workspace, reset-workspaces). A bare boolean is shorthand for { enable }. Default enable: true (like the other capabilities); the renderer still forces the command gate off in single-layout mode (no workspaces prop), so a dispatch there is a no-op. Reset commands additionally no-op without workspaceDefaults. followMovedLeaf is the default follow on move-leaf-to-workspace when the command omits it (Hyprland movetoworkspace vs movetoworkspacesilent); default false.

TilingKeyBinding

interface

A single keyboard binding: an arbitrary chord mapped to an arbitrary command. The public chord→command registration surface (the Hyprland bind analog), distinct from the fixed TilingKeymap chord overrides (which only re-chord the built-in action set). The chord matches on KeyboardEvent.code exactly like TilingKeyChord.

Signature:

interface TilingKeyBinding 

Properties

Property

Modifiers

Type

Description

chord

TilingKeyChord

The chord that triggers this binding (matched on KeyboardEvent.code).

command

TilingCommand

The command dispatched when the chord matches.

TilingKeyBindings

interface

Consumer keyboard-binding registry. bindings augment (and, on a chord collision, override) the default keymap bindings; setting replaceDefaults suppresses the built-in keymap path entirely so ONLY these bindings are live.

Signature:

interface TilingKeyBindings 

Properties

Property

Modifiers

Type

Description

bindings?

ReadonlyArray<TilingKeyBinding>

(Optional) Consumer chord→command bindings; augment (or override on collision) the defaults.

replaceDefaults?

boolean

(Optional) When true, the default keymap bindings are NOT consulted. Default false (augment).

TilingKeyChord

interface

A single keyboard chord: a PHYSICAL KeyboardEvent.code value plus the modifier state required to match it. Absent modifiers resolve to false (the modifier must NOT be held) — a chord fully specifies its modifier requirements.

The chord matches on event.code (the physical key, e.g. "Enter", "Escape", "BracketLeft", "BracketRight"), NOT event.key (the produced character). This is mandatory on macOS, where holding Option(Alt) rewrites event.key into dead-key glyphs (Option+] → "‘", Option+digits → special glyphs), so a event.key-based comparison never matches. event.code is stable across keyboard layouts and modifier state.

Signature:

interface TilingKeyChord 

Properties

Property

Modifiers

Type

Description

alt?

boolean

(Optional) Require the Alt key. Default false.

code

string

Matches KeyboardEvent.code (e.g. "Enter", "Escape", "BracketLeft", "BracketRight", "Digit1").

ctrl?

boolean

(Optional) Require the Ctrl key. Default false.

meta?

boolean

(Optional) Require the Meta (Cmd / Win) key. Default false.

shift?

boolean

(Optional) Require the Shift key. Default false.

TilingKeyChordModifiers

interface

Modifier requirements for the jump-to-pane digit family (Alt+1..Alt+9). The 1..9 digit is implied (matched against the physical Digit1..Digit9 codes); only the modifier state is configurable.

Signature:

interface TilingKeyChordModifiers 

Properties

Property

Modifiers

Type

Description

alt?

boolean

(Optional) Require the Alt key. Default false.

ctrl?

boolean

(Optional) Require the Ctrl key. Default false.

meta?

boolean

(Optional) Require the Meta (Cmd / Win) key. Default false.

shift?

boolean

(Optional) Require the Shift key. Default false.

TilingKeymap

interface

Public, reactive keymap. Every field is optional; an undefined field resolves to its documented default via resolveKeymap. Action-level merge: supplying one binding leaves the others at their defaults.

Signature:

interface TilingKeymap 

Properties

Property

Modifiers

Type

Description

cycleLayoutMode?

TilingKeyChord

(Optional) Toggle the focused subtree's layout mode dwindle ⇄ master (the Hyprland layout-toggle analog). Default Alt+L (code KeyL). Only meaningful when masterLayout is enabled.

cycleMasterOrientation?

TilingKeyChord

(Optional) Cycle the master-area orientation left → top → right → bottom → left. Default Alt+Shift+O (code KeyO + Shift). Bare Alt+O is avoided because Chrome/Edge on Windows/Linux reserve it for the browser menu. Switches to master layout mode automatically when invoked from dwindle.

decrementMasterCount?

TilingKeyChord

(Optional) Remove one tile from the master area (-1 master count). Default Alt+- (code Minus).

decrementMasterRatio?

TilingKeyChord

(Optional) Shrink the master-area fraction (-0.05 ratio). Default Alt+, (code Comma).

enterMoveMode?

TilingKeyChord

(Optional) Enter keyboard MOVE MODE on the focused pane (the keyboard analog of a drag pickup). Default Alt+M (code KeyM). While in move mode the focus arrows pick a destination, Enter commits the relocation (insertLeafAdjacent), and Escape cancels. Only meaningful when rearrange is enabled.

focusCurrentOrLast?

TilingKeyChord

(Optional) Toggle focus between the current pane and the most-recently-focused other pane (the MRU "focus current-or-last" toggle; Hyprland focuscurrentorlast analog). Default Alt+\`` (code Backquote). Only meaningful when focus` is enabled.

focusDown?

TilingKeyChord

(Optional) Move focus to the geometric neighbor BELOW. Default bare ArrowDown.

focusLeft?

TilingKeyChord

(Optional) Move focus to the geometric neighbor on the LEFT. Default bare ArrowLeft.

focusRight?

TilingKeyChord

(Optional) Move focus to the geometric neighbor on the RIGHT. Default bare ArrowRight.

focusUp?

TilingKeyChord

(Optional) Move focus to the geometric neighbor ABOVE. Default bare ArrowUp.

groupTabNext?

TilingKeyChord

(Optional) Activate the next member tab of the focused group. Default Alt+K (code KeyK).

groupTabPrevious?

TilingKeyChord

(Optional) Activate the previous member tab of the focused group. Default Alt+J (code KeyJ).

incrementMasterCount?

TilingKeyChord

(Optional) Add one tile to the master area (+1 master count). Default Alt+= (code Equal).

incrementMasterRatio?

TilingKeyChord

(Optional) Grow the master-area fraction (+0.05 ratio). Default Alt+. (code Period).

jumpToPane?

TilingKeyChordModifiers

(Optional) Modifier set for the Alt+1..Alt+9 jump family. Default Alt.

nextPane?

TilingKeyChord

(Optional) Cycle to the next pane. Default Alt+].

previousPane?

TilingKeyChord

(Optional) Cycle to the previous pane. Default Alt+[.

restore?

TilingKeyChord

(Optional) Restore from maximize. Default Escape.

toggleGroup?

TilingKeyChord

(Optional) Toggle grouping for the focused pane: group it with its reading-order neighbor, or ungroup it if already grouped (the Hyprland togglegroup analog). Default Alt+G (code KeyG). Only meaningful when grouping is enabled.

toggleMaximize?

TilingKeyChord

(Optional) Toggle maximize on the focused pane. Default Alt+Enter (code Enter).

TilingLayoutContainerSize

interface

Container geometry for the integrity/repair pass performed on load/commit.

Signature:

interface TilingLayoutContainerSize 

Properties

Property

Modifiers

Type

Description

containerHeightPx

number

Root container height in CSS pixels.

containerWidthPx

number

Root container width in CSS pixels.

TilingLayoutMode

type

Arrangement of a split subtree's slots:

  • "dwindle" — the default recursive binary-split layout: each split distributes its two children along its axis by ratio (the original and only Phase-1/2 algorithm). - "master" — a master-area + stack layout (the Hyprland master analog). The split's descendant slots (leaves / groups, flattened in reading order) are laid out as masterCount master tiles in a master area plus the remaining tiles in a stack; the split's ratio is reused as the master-area fraction and masterOrientation places the master area. The binary structure beneath a master split is flattened (ignored for geometry); it still defines slot membership + order + identity, and the reducers still operate on it.

Signature:

type TilingLayoutMode = "dwindle" | "master";

TilingLayoutStorage

interface

Minimal synchronous key/value store, structurally compatible with the Web Storage API (window.localStorage / sessionStorage). Supply a custom implementation for tests, SSR, or a non-localStorage backend.

Signature:

interface TilingLayoutStorage 

Methods

Method

Description

getItem(key)

Read the serialized layout tree stored under key, or null if absent.

removeItem(key)

Remove any serialized layout tree stored under key.

setItem(key, value)

Write the serialized layout tree value under key.

TilingLeafDropPreview

interface

A projected landing/result preview shown on a pane during a drag.

Signature:

interface TilingLeafDropPreview 

Properties

Property

Modifiers

Type

Description

mode

TilingLeafPreviewMode

Whether the preview depicts a swap, an edge-insert, or a group-merge.

partnerLeafId

string

The other leaf involved (swap counterpart or insert neighbor).

role

TilingLeafPreviewRole

Whether this preview is the drag-source landing or the drop-target result.

zone

TilingLeafDropZone

The drop zone the preview corresponds to.

TilingLeafDropZone

type

The five drop regions of a pane: a central swap zone plus four insert edges.

Signature:

type TilingLeafDropZone = "center" | "left" | "right" | "top" | "bottom";

TilingLeafPreviewMode

type

Whether a preview depicts a swap, an edge-insert, or a group-merge result.

Signature:

type TilingLeafPreviewMode = "swap" | "edge-insert" | "group-merge";

TilingLeafPreviewRole

type

Which side of a preview a shadow represents (drag source vs drop target).

Signature:

type TilingLeafPreviewRole = "drag-source-landing-shadow" | "drop-target-result-shadow";

TilingMasterOrientation

type

Where the master area sits in layoutMode: "master". left/right divide the container along WIDTH (master area a column, members stacked vertically); top/bottom divide along HEIGHT (master area a row, members stacked horizontally). The complement holds the stack.

Signature:

type TilingMasterOrientation = "left" | "right" | "top" | "bottom";

TilingMaximizeCapability

interface

Maximize (expand-to-viewport) capability. The maximize render-mode is non-destructive — it hides sibling panes and renders the focused pane filling the tiling viewport without mutating the layout tree.

Signature:

interface TilingMaximizeCapability 

Properties

Property

Modifiers

Type

Description

enable?

boolean

(Optional) Enable the per-pane maximize/restore control + shortcuts. Default true.

keepGroupTabStrip?

boolean

(Optional) When the maximized leaf belongs to a group, render the GROUP node at viewport size (strip above the maximized member) instead of the leaf alone. Default true. The group's activeMemberId is overridden at render time to the maximized leaf — the layout tree is not mutated. grouping.showGroupTabStrip: false still hides the strip. Set false to restore leaf-only maximize (no group chrome).

keymap?

Pick<TilingKeymap, "toggleMaximize" | "restore">

(Optional) Per-capability keybinding overrides for toggleMaximize / restore. These take precedence over the top-level keymap, which takes precedence over the documented defaults.

TilingMinBBoxPx

interface

Leaf-scoped minimum bounding-box floor (CSS px), one optional component per dimension. See TilingLeafNode.minBBoxPx for the precedence this floor takes over TilingSplitNode.minPaneSizePx and TilingLayoutConfig.minPaneSizePx.

Signature:

interface TilingMinBBoxPx 

Properties

Property

Modifiers

Type

Description

heightPx?

number

(Optional) Minimum height (CSS px) this leaf's pane must retain during resize.

widthPx?

number

(Optional) Minimum width (CSS px) this leaf's pane must retain during resize.

TilingMovePlacement

type

Which edge of a target pane an inserted/moved pane lands on.

Signature:

type TilingMovePlacement = "left" | "right" | "top" | "bottom";

TilingOnExternalDrop

type

Fired on release while TilingRendererCommonProps.externalDragHover is non-null, **before** the drag FSM settles, so the host can claim the leaf synchronously and skip the cancel fly-back. Return false to DECLINE the claim (the target refused the leaf): the release then settles through the ordinary cancel fly-back. void / true claims.

Signature:

type TilingOnExternalDrop = (leafId: string, targetId: string, point: TilingClientPoint, hover: TilingExternalDragHover) => void | boolean;

References: TilingClientPoint, TilingExternalDragHover

TilingOverlayPortalContainer

type

Host node for the renderer's position: fixed drag overlays (ghost, custom cursor, cancel fly-back). An element, null (fall back to document.body), or a thunk evaluated every render so a late-mounted container (ref / callback) is picked up without remounting the renderer. See TilingRendererCommonProps.overlayPortalContainer for the containing-block caveat, SSR null, and stacking-context z-index note.

Signature:

type TilingOverlayPortalContainer = HTMLElement | null | (() => HTMLElement | null);

TilingPaneAction()

function

A header action button (maximize, group, …) that stopPropagations on both pointer-down and click, so activating it never starts a drag or steals pane focus. Defaults type to "button" and calls your onClick after stopping propagation. Bring your own className / style / children.

Signature:

declare function TilingPaneAction(input: TilingPaneActionProps): React.ReactElement;

Parameters

Parameter

Type

Description

{ onClick, type, ...rest }

(not declared)

input

TilingPaneActionProps

Returns:

React.ReactElement

TilingPaneActionProps

type

Props for TilingPaneAction().

Signature:

type TilingPaneActionProps = React.ButtonHTMLAttributes<HTMLButtonElement>;

TilingPaneBody()

function

The body wrapper of a custom pane. Always renders its wrapper <div> (so the pane keeps its layout), but renders children ONLY when paneBodyRenderMode === "render-content". This keeps a custom pane aligned with the renderer's drag-ghost / hidden-body semantics — the ghost reuses the same render path (overlay portal container, default document.body), so an empty body never rides along. Bring your own className / style.

HT-PANE-COLLAPSE: while pane.isCollapsed is true (and the pane is NOT maximized — see below) the wrapper is forced to display: none (merged into any consumer style) rather than merely emptying its children. A collapsed leaf is pinned to the chrome extent (titlebar-only, along its parent split's axis), but a bare empty body still keeps its flex-1 / min-h-0 box in the layout — the leftover slack between that box and the pinned extent rendered as a dead strip beside/below the titlebar. Hiding the box entirely removes that strip so the collapsed pane is exactly titlebar-sized.

HT-COLLAPSE-BODY-MODE: this display: none hide is INDEPENDENT of whether children are mounted. With the default collapseBodyMode: "keep-mounted", resolvePaneBodyRenderMode resolves "render-content" while collapsed, so children stay mounted (state/scroll/focus survive collapse/expand) behind the display: none wrapper; "unmount" resolves "render-empty" instead, removing children from the tree exactly like the legacy behavior.

HT-PANE-COLLAPSE + maximize: pane.isMaximized SUSPENDS the display: none gate for exactly this leaf, matching resolvePaneBodyRenderMode's own maximize override — otherwise a maximized collapsed pane would render render-content children into a wrapper still forced invisible, i.e. a titlebar strip in an empty full-screen frame.

Signature:

declare function TilingPaneBody(input: TilingPaneBodyProps): React.ReactElement;

Parameters

Parameter

Type

Description

{ pane, children, style, ...rest }

(not declared)

input

TilingPaneBodyProps

Returns:

React.ReactElement

TilingPaneBodyProps

interface

Props for TilingPaneBody().

Signature:

interface TilingPaneBodyProps extends React.HTMLAttributes<HTMLDivElement> 

Extends: React.HTMLAttributes<HTMLDivElement>

Properties

Property

Modifiers

Type

Description

pane

Pick<TilingRenderTileProps, "paneBodyRenderMode" | "isCollapsed" | "isMaximized">

The renderTile args for this pane (paneBodyRenderMode, isCollapsed, and isMaximized are read). Pass the whole args object.

TilingPaneBodyRenderMode

type

Resolved pane-body render decision: - "render-content" — paint the tile body. Also resolved for a COLLAPSED pane under the default collapseBodyMode: "keep-mounted" (HT-COLLAPSE-BODY-MODE) — TilingPaneBody() still visually hides it via its own independent display: none gate. - "render-empty" — body removed from the tree (content toggle off, OR a collapsed pane under collapseBodyMode: "unmount"). - "render-reservation" — empty ghost-seat reservation slot during a drag.

Signature:

type TilingPaneBodyRenderMode = "render-content" | "render-empty" | "render-reservation";

TilingPaneCollapsedChangeEvent

interface

The payload of TilingRendererCommonProps.onPaneCollapsedChange: which leaf changed collapse state and its NEW state. Emitted after the collapse edit is reported via onLayoutChange, so a host reacting here (e.g. retitling the pane) reads a consistent post-edit world.

Signature:

interface TilingPaneCollapsedChangeEvent 

Properties

Property

Modifiers

Type

Description

collapsed

readonly

boolean

The leaf's new collapse state — true collapsed to titlebar-only, false expanded.

leafId

readonly

string

The leaf node id whose collapse state changed.

TilingPaneCycleDirection

type

Pane cycle direction for the reading-order ring (next / previous, wraparound).

Signature:

type TilingPaneCycleDirection = "next" | "previous";

TilingPaneIdentityMode

type

Pane identity binding — WHICH React instance renders a tile, and whether it survives the tile's slot moving in the split tree.

  • "slot" — a pane is rendered in place at its leaf's tree position. React reconciles the recursive split tree POSITIONALLY, so any edit that moves a leaf to a different branch (an insert drop, a group fold, a master-stack reorder) unmounts the pane at the old position and mounts a fresh instance at the new one; a swap keeps both instances but hands each the OTHER tile's props. Host content re-initializes (the "loading flash" after a drop). This is the legacy behavior and the only SSR-faithful one (server HTML carries the pane content in-slot). - "stable" — panes are rendered ONCE each in a hidden, tile-keyed pool and their DOM node is RELOCATED into whichever slot currently shows their tile (a layout-effect appendChild, never a React re-parent). The React instance — hooks, state, refs, subscriptions, iframes, scroll positions — is the same object through drag → drop → settle and every other tree edit; only the DOM parent changes. During a live drag the picked-up pane is parked in the pool (its slot shows the content-less seat; the single ghost paints the pane through the overlay portal container, default document.body, see TilingRendererCommonProps.overlayPortalContainer) and reseats on drop. Server render emits EMPTY slots (content is placed on the client), so hydration-time flash is the cost. - "auto" (default) — "stable" on a client-only mount, "slot" when the first render is a hydration of server markup.

Signature:

type TilingPaneIdentityMode = "auto" | "stable" | "slot";

TilingPaneRoot()

function

The root element of a custom pane. Renders an <article data-leaf-id> (the attribute the renderer resolves the drag source from) and wires the pane's onFocus (on both focus and click), onPointerMove, and onPointerLeave handlers, so focus, resize, and pre-drag hover telemetry keep working. Bring your own className / style / children; defaults tabIndex to -1.

Signature:

declare function TilingPaneRoot(input: TilingPaneRootProps): React.ReactElement;

Parameters

Parameter

Type

Description

{ pane, className, ...rest }

(not declared)

input

TilingPaneRootProps

Returns:

React.ReactElement

TilingPaneRootProps

interface

Props for TilingPaneRoot().

Signature:

interface TilingPaneRootProps extends Omit<React.HTMLAttributes<HTMLElement>, "onFocus" | "onClick" | "onPointerMove" | "onPointerLeave"> 

Extends: Omit<React.HTMLAttributes<HTMLElement>, "onFocus" | "onClick" | "onPointerMove" | "onPointerLeave">

Properties

Property

Modifiers

Type

Description

pane

Pick<TilingRenderTileProps, "leafId" | "onFocus" | "onPointerMove" | "onPointerLeave">

The renderTile args for this pane (only its leafId and the focus/hover handlers are read). Pass the whole args object; the primitive wires the root correctly.

TilingPaneSizing

interface

Per-dimension sizing declaration for a node. Each dimension is independent: a pane can be static in height, in width, or in both. An undefined dimension defaults to "flexible".

The object shape is chosen over an axis list (e.g. staticAxes: ("width" | "height")[]) because it is explicit per-dimension, self-documenting at the call site (sizing: { height: "static" }), and lets a dimension be stated "flexible" explicitly rather than only inferred from absence.

Signature:

interface TilingPaneSizing 

Properties

Property

Modifiers

Type

Description

height?

TilingPaneSizingMode

(Optional) Sizing mode for the HEIGHT dimension. Undefined → "flexible".

heightPx?

number

(Optional) Pinned HEIGHT in CSS px, captured from the pane's measured bounding box at the moment the user freezes the pane via the title-bar STATIC H / BOTH control. Only meaningful when height === "static". See widthPx.

width?

TilingPaneSizingMode

(Optional) Sizing mode for the WIDTH dimension. Undefined → "flexible".

widthPx?

number

(Optional) Pinned WIDTH in CSS px, captured from the pane's measured bounding box at the moment the user freezes the pane via the title-bar STATIC W / BOTH control. Only meaningful when width === "static". When present, the static-width pane renders at exactly this pixel extent (a measured bbox FREEZE) instead of being content-sized. Undefined → the static dimension is content-sized (the legacy intrinsic behavior).

TilingPaneSizingMode

type

Per-dimension sizing mode for a node placed inside a split.

  • "flexible" — the node shares space by ratio along the split axis and stretches/reflows along the cross axis (the default for every dimension). - "static" — the node is sized to its INTRINSIC/CONTENT extent along that dimension; it is excluded from ratio distribution and resize dividers when the static dimension runs ALONG the parent split axis, and simply content-sizes (no stretch) when the static dimension is the CROSS axis.

Signature:

type TilingPaneSizingMode = "static" | "flexible";

TilingPaneSwitchingCapability

interface

Tab-like pane-switching capability. Renders a tab strip across the top of the tiling region and binds the cycle / jump shortcuts. When maximized, switching panes also switches which pane is maximized.

Signature:

interface TilingPaneSwitchingCapability 

Properties

Property

Modifiers

Type

Description

enable?

boolean

(Optional) Enable pane switching (tab strip + cycle/jump shortcuts). Default true.

keymap?

Pick<TilingKeymap, "previousPane" | "nextPane" | "jumpToPane">

(Optional) Per-capability keybinding overrides for previousPane / nextPane / jumpToPane. These take precedence over the top-level keymap.

multiSelectGrouping?

boolean

(Optional) Alt/Opt+click a pane HEADER to toggle that pane into a multi-selection set, then group the selected panes into one flat tabbed group via the existing group-leaves command (the same chord family as the Alt+G group key). Default true. A plain (no-modifier) header click keeps its normal focus/drag-pickup behavior; only altKey (Opt on macOS, Alt on Windows/Linux) presses participate in multi-selection. When ≥2 groupable panes are selected, a "Group" control appears in each selected pane's header; clicking it dispatches group-leaves (the clicked pane is the host slot) and clears the selection. The selection also clears on Escape, on a drag pickup, and on a plain click that establishes a single focus. When false, Alt/Opt+click and the Group control are no-ops. The grouping itself is independently gated by the grouping capability — with grouping disabled the Group control is suppressed (the multi-select dispatch is a safe no-op).

showContentToggle?

boolean

(Optional) Render the tab strip's pane-content visibility checkbox (the "content" toggle that flips the default-tile body between content and empty). Default true. Set false to suppress the control for embeddings that always paint their own content via a custom renderTile (e.g. a docs/SEO surface), where the toggle is inert chrome. Suppressing the toggle hands content ownership to the embedding: pane-content is treated as VISIBLE by default (isPaneContentVisible initializes true) for ALL drag surfaces — in-tree panes, the source slot, the hop-in slot, and the portaled drag ghost (the overlay portal container, default document.body, see TilingRendererCommonProps.overlayPortalContainer) — so the ghost body matches the seated body. With no control rendered, the flag cannot be flipped off, so this default holds for the lifetime of the embedding. (Ghost-seat reservation slots still render empty — that is a drag mechanic resolved by resolvePaneBodyRenderMode, independent of this flag.)

showSwitcherOverlay?

boolean

(Optional) Render the macOS Cmd+Tab-style visual switcher overlay while cycling panes with a held modifier (Alt+] / Alt+[). Default true. When true, a cycle press opens a centered overlay listing the panes and advances the highlight; the selection commits (focus / activate, and switch the maximized pane when maximized) on modifier release, and Escape cancels. When false, cycle presses activate the next/previous pane immediately (no overlay). The shortcuts work regardless of this flag.

showTabStrip?

boolean | "maximized"

(Optional) Render the TOP-LEVEL tab strip (the single strip across the tiling region listing every pane). Default true. true / false behave as always-on / hidden; "maximized" renders the strip only while a pane is maximized. The cycle/jump shortcuts work regardless. This flag does NOT govern the per-group tab strip a tabbed-stacking group renders above its active member — that strip is governed by grouping.showGroupTabStrip (see TilingGroupingCapability).

tabDoubleClickMaximize?

boolean

(Optional) Toggle a pane's maximize/restore state when its TAB in the tab strip is double-clicked. Default true. The double-click dispatches the SAME toggle-maximize command (targeting the tab's leaf) the Alt+Enter keybinding dispatches, so the two converge on one maximize state — a double-click maximizes, a second double-click (or Alt+Enter, or Escape) restores. Single-click tab activation is unaffected. Also gated by the maximize capability: with maximize.enable: false the double-click is a no-op even when this is true. Set false to opt out (single-click tab activation only).

tabStrip?

TilingPaneTabStripOptions

(Optional) Built-in top-level pane tab strip. Ignored when showTabStrip is false. Omitted fields resolve to the defaults in tabStrip below (placement "top", group-strip height and theme).

TilingPaneTab

interface

One entry of the top-level pane tab strip. A group contributes a single tab, labelled by the maximized-or-active member.

Signature:

interface TilingPaneTab 

Properties

Property

Modifiers

Type

Description

active

boolean

Whether this tab's leaf is the focused pane.

groupId

string | null

Group id when this tab stands for a group; null for a loose leaf.

leafId

string

Outer-layout leaf id this tab activates.

maximized

boolean

Whether this tab's leaf is the maximized pane.

memberCount

number

Group member count when grouped; 1 for a loose leaf.

ordinal

number

1-based position in the strip (reading order).

tileId

string

Tile id that leaf displays.

title

string

Tile title, or the leaf id when the tile has no title.

TilingPaneTabStripOptions

interface

Built-in top-level pane tab strip. Shares the group-strip theme vocabulary (TilingGroupTabStripTheme) and the same role="tablist" keyboard model. Omitted fields resolve to placement "top", the group-strip default height, and the pane-strip theme defaults (solid active pill).

Signature:

interface TilingPaneTabStripOptions 

Properties

Property

Modifiers

Type

Description

height?

number

(Optional) Strip height in CSS pixels. Default is the group-strip height (28).

placement?

"top" | "bottom"

(Optional) "top" (default) paints the strip above the tiling viewport. "bottom" paints the same strip under the viewport.

renderTabLabel?

(tab: TilingPaneTab) => React.ReactNode

(Optional) Custom tab label. Default is the tile title. The string title is always the tab's title attribute (ellipsis tooltip).

theme?

TilingGroupTabStripTheme

(Optional) CSS-value tokens. Omitted tokens keep the pane-strip library default.

TilingPaneTitleBarContent()

function

Middle slot of a custom pane titlebar — between the title (left) and native window controls (right). Stops pointer-down propagation ONLY when the press lands on a genuine interactive control inside the slot (button, link, input/textarea/select, role="button", or a contenteditable node — see isInteractiveControlTarget), so a real toolbar click / find input does not start a rearrange drag from the surrounding TilingDragHandle(). A press anywhere ELSE in the slot — decorative labels, badges, a selected- case id, any non-interactive content — falls through untouched, so the titlebar stays draggable from the whole slot minus its actual controls (HT-TITLEBAR-DRAG-THRU: a blanket stop here previously turned any non-empty titleBarContent into a dead zone that swallowed drag pickup entirely, even where nothing inside it was interactive).

Unstyled: bring className (typically min-w-0 flex-1 so the slot fills remaining header width and truncates). Prefer reading tile.titleBarContent from TilingRenderTileProps and wrapping it here.

Signature:

declare function TilingPaneTitleBarContent(input: TilingPaneTitleBarContentProps): React.ReactElement;

Parameters

Parameter

Type

Description

{ onPointerDown, ...rest }

(not declared)

input

TilingPaneTitleBarContentProps

Returns:

React.ReactElement

TilingPaneTitleBarContentProps

interface

Props for TilingPaneTitleBarContent().

Signature:

interface TilingPaneTitleBarContentProps extends React.HTMLAttributes<HTMLDivElement> 

Extends: React.HTMLAttributes<HTMLDivElement>

TilingPaneTitleBarControlsCapability

interface

Per-pane TITLE-BAR control capability. These controls render directly in each pane's own header (next to maximize), so they target THAT pane (no central target-pane selector). Two independent control groups:

  • sizing — the FLEX / STATIC H / STATIC W / BOTH segmented control. STATIC actions measure the pane's current rendered bbox and pin the chosen dimension(s) to that pixel value; FLEX clears the pin. - acquireSpace — the four directional (→ ← ↑ ↓) "grow to claim space" buttons, driven by the growLeafToward reducer.

Signature:

interface TilingPaneTitleBarControlsCapability 

Properties

Property

Modifiers

Type

Description

acquireSpace?

boolean

(Optional) Render the per-pane directional (→ ← ↑ ↓) acquire-space controls. Default true.

collapse?

boolean

(Optional) Render the per-pane COLLAPSE-to-titlebar control + enable the toggle-collapse / set-collapsed commands (HT-PANE-COLLAPSE). Default false — collapse is OPT-IN for backward compatibility (an existing consumer's title-bars keep exactly today's control set until it turns this on), mirroring the paneSwitching.showContentToggle opt-in exception.

sizing?

boolean

(Optional) Render the per-pane FLEX / STATIC H / STATIC W / BOTH sizing control. Default true.

TilingRendererCommonProps

interface

The mode-independent TilingRenderer props — everything except the layout source. Extended by TilingRendererProps (single controlled layout) and TilingRendererWorkspaceSetProps (controlled TilingWorkspaceSet).

Signature:

interface TilingRendererCommonProps 

Properties

Property

Modifiers

Type

Description

chromeFocusOutline?

TilingChromeFocusOutline

(Optional) How native UA focus outlines are handled on the renderer's own chrome. Undefined resolves to "suppress" (inject the scoped .hpt-root stylesheet). Set "native" to keep browser focus rings. Only ever affects chrome inside .hpt-root; app chrome is untouched. See TilingChromeFocusOutline.

className?

string

(Optional) Optional class name applied to the tiling root element.

config

TilingLayoutConfig

Global geometry configuration (gap, min pane size, handle size).

dragAnimationEnabled?

boolean

(Optional) Master gate for all drag-motion choreography. When false, the ghost hop, survivor reflow, pickup-entrance, and swap dip collapse to instant placement (durations → ~0). Default true. The per-knob values (speeds, bounce) are preserved and re-apply when re-enabled.

dragGhostMode?

TilingDragGhostMode

(Optional) Live-drag ghost presentation. Default "footprint" — the tile-sized ghost, byte-identical to 26.9.1. "compact" always paints the chip; "auto" collapses to the chip while TilingRendererCommonProps.externalDragHover is non-null and expands back to the footprint when it clears.

dragHopEasing?

string

(Optional) CSS <easing-function> for the dragged GHOST's hop transit (hop-in / hop-out / pickup entrance) and the swap/edge-insert ghost motion. Undefined → the default snappy decel cubic-bezier(0.2, 0.8, 0.2, 1). An invalid / empty string falls back to that default (never reaches the compositor as a broken transition). The seated-ghost magnetic linear() curve and the swap-bounce curve are NOT replaced by this knob (sampled non-bezier curves).

dragReflowEasing?

string

(Optional) CSS <easing-function> for the affected ("survivor") panes' FLIP reflow settle. Undefined → defaults to the same curve as dragHopEasing so the ghost and survivors read as one coordinated motion. Invalid / empty falls back to the default.

externalDragHover?

TilingExternalDragHover | null

(Optional) Host tells the engine the pointer is over an external drop target (and where). In "auto" ghost mode this is what toggles compact ↔ footprint. On release, a non-null value plus TilingRendererCommonProps.onExternalDrop claims the drag (no cancel fly-back).

focusedLeafId?

string | null

(Optional) Controlled focused-leaf id. undefined → uncontrolled; null → nothing focused.

ghostTransitSpeedPercent?

number

(Optional) Speed of the dragged GHOST's transit animation (hop-in / hop-out / pickup entrance) as a percent of the 170ms baseline (100 = baseline). Lower is slower; higher is faster. Clamped to [10, 400]. Default 100.

interaction?

TilingInteractionCapabilities

(Optional) Reactive interaction-capability flags. Undefined resolves to all-enabled. Changing this prop at runtime updates renderer behavior immediately.

maximizedLeafId?

string | null

(Optional) Controlled maximized-pane id. undefined → uncontrolled (renderer-managed internal state). null → controlled, nothing maximized. A leaf id → controlled, that pane maximized. Reacts to prop changes without remount.

onExternalDragHoverChange?

(hover: TilingExternalDragHover | null) => void

(Optional) Notified when the hover produced by TilingRendererCommonProps.resolveExternalDragHover changes identity (kind / targetId / workspaceId) — including null when the pointer leaves every target or the drag settles. Not fired for a host-supplied externalDragHover.

onExternalDrop?

TilingOnExternalDrop

(Optional) Synchronous claim hook. Fired on POINTER_UP while TilingRendererCommonProps.externalDragHover is non-null, **before** the FSM settles, so the host can take the leaf (move it off this tree) and the engine marks the drag claimed — DragCancelOverlay is skipped. Absent → existing cancel / fly-back behaviour even when hover is set. Return false to decline the claim.

onFocusedLeafChange?

(leafId: string) => void

(Optional) Notified whenever the focused leaf changes.

onMaximizedLeafChange?

(leafId: string | null) => void

(Optional) Notified whenever the maximized pane changes (null on restore).

onPaneCollapsedChange?

(event: TilingPaneCollapsedChangeEvent) => void

(Optional) Notified whenever a pane's COLLAPSE state changes (HT-PANE-COLLAPSE) — fired on every toggle-collapse / set-collapsed that actually flips a leaf, from the title-bar control, a keyboard binding, or an imperative dispatch. The event-driven hook a host uses to react to collapse/expand (e.g. to swap a pane's title between a short collapsed label and its full expanded label by updating its own tiles state). The layout edit itself still flows through onLayoutChange; this callback is the semantic notification alongside it.

onThemeChange?

(themeId: TilingThemeId) => void

(Optional) Notified when the in-renderer theme switcher (top-bar control) requests a different theme. Present this control only when wired; the consumer owns the themeId state (controlled). Omit to hide the switcher — the theme stays whatever themeId (or the theme prop) resolves to. Generic mechanism; the demo theme composition lives with the consumer. BUILT-INS ONLY: the switcher enumerates the built-in registry and reports TilingThemeId members — a consumer running a custom theme object does not wire this callback (its theme never appears in the switcher).

onTileAccentChange?

(tileId: string, accent: TilingTileAccent) => void

(Optional) When provided, the top-bar tab strip surfaces an accent palette picker (the enumerable TILING_TILE_ACCENTS swatches) that recolors the currently-focused pane's tile. The renderer resolves the focused tile id; the consumer owns the tile registry and applies the new accent (e.g. by updating its tiles state). Omit to hide the picker — accent remains a static per-tile property. Generic mechanism; the demo palette composition lives with the consumer.

overlayPortalContainer?

TilingOverlayPortalContainer

(Optional) DOM node that hosts the position: fixed drag overlays (ghost, custom cursor, cancel fly-back). Default document.body. Accepts an element, null (fall back to document.body), or a thunk evaluated every render so a late-mounted container (ref / callback) is picked up without remounting the renderer. On the server the resolver returns null and mounts nothing — overlays only paint during a client, post-hydration drag.

Two independent reasons the default is document.body: (1) containing- block immunity — position: fixed stays window-relative; (2) the ghost stays outside the React root's event-delegation scope. Redirecting the DOM mount does not change (2): React still walks fibers, not the DOM, and measurement selectors stay root/viewport-scoped. React context (TilingThemeProvider, etc.) still propagates through createPortal.

Containing-block caveat: the container MUST NOT sit under an ancestor with transform, filter, backdrop-filter, perspective, contain: paint (or layout / strict / content), or will-change of those. The overlays place themselves with window-relative client coordinates; any ancestor that establishes a containing block for position: fixed reintroduces ghost↔seat drift.

Stacking: overlay z-indexes (ghost 220, cursor 230, cancel 219) are relative to the container's stacking context, not the document.

paneIdentity?

TilingPaneIdentityMode

(Optional) How a pane's React instance is bound to its slot in the split tree — see TilingPaneIdentityMode. Default "auto": "stable" on a client-only mount (host content survives drag → drop → settle and every other tree edit without remounting), "slot" when the renderer hydrates server-rendered markup (the SSR-faithful legacy behavior).

projectedOverlayBackgroundAlpha?

number

(Optional) Background opacity [0, 1] for the projected landing overlays.

renderTile?

(args: TilingRenderTileProps) => React.ReactNode

(Optional) Custom pane renderer. Receives TilingRenderTileProps; omit to use the default tile.

resolveExternalDragHover?

TilingExternalDragHoverResolver

(Optional) Engine-driven alternative to TilingRendererCommonProps.externalDragHover: the renderer calls this with the pointer position on every processed drag frame (and synchronously at release) and treats the result as the external hover. A non-null externalDragHover prop takes precedence. The resolved hover clears when the drag settles.

showDropBorderHints?

boolean

(Optional) Whether drop border hints are painted on candidate panes during a drag.

showDropPreviewOverlays?

boolean

(Optional) Whether translucent projected landing overlays are shown during a drag.

survivorReflowSpeedPercent?

number

(Optional) Speed of the affected ("survivor") panes' REFLOW transform animation as a percent of the 170ms baseline (100 = baseline). Lower is slower; higher is faster. Clamped to [10, 400]. Default 100. When this equals ghostTransitSpeedPercent the two parties are at PARITY, which the coherent non-intersecting transit dip requires (see coherentTransit).

swapBounceMagnitudePercent?

number

(Optional) Swap-landing bounce magnitude as a percent of full overshoot (0 = no overshoot, today's monotonic settle; 100 = pronounced bounce). Applies an easeOutBack overshoot to the ghost seated hop-in and the survivor reflow settle. Per-element (no cross-element coupling), so it is NOT gated by speed parity. Inert while the coherent-transit dip owns the landing. Clamped to [0, 100]. Default 0. Skipped under prefers-reduced-motion.

theme?

TilingTheme

(Optional) Full consumer-authored theme; takes precedence over themeId. Every renderer-painted surface (pane shells, header, focus frame, ghost, dividers, root, tab strip) and every accent-composition resolver comes from this object, so a consumer owns the complete chrome without touching library internals. Undefined → the built-in theme selected by themeId.

themeId?

TilingThemeId

(Optional) Active visual theme id. Selects which built-in TilingTheme paints every renderer surface (pane shells, header, focus frame, ghost, dividers, root, tab strip) and how per-pane accents compose with it. Undefined resolves to the library default ("neon-terminal"). Reacts to prop changes without remount — a live switch re-themes the whole tree.

tiles

ReadonlyArray<TilingTile> | ReadonlyMap<string, TilingTile>

Tile registry, accepted as either an ordered array (resolved by id) or a Map keyed by tile id. A dashboard can pass a plain ReadonlyArray of { id, title, content } tiles; the interactive lab passes a Map.

TilingRendererWorkspaceSetProps

interface

Props for TilingRenderer in WORKSPACE-SET mode: the host holds a TilingWorkspaceSet and the renderer paints the ACTIVE workspace's tree. Every tree edit is reported as a whole next set through onWorkspacesChange (the active workspace's layout replaced via setWorkspaceLayout), so the host reducer stays a single set → set step.

Signature:

interface TilingRendererWorkspaceSetProps extends TilingRendererCommonProps 

Extends: TilingRendererCommonProps

Remarks

Switching workspaces.activeId does NOT remount panes whose leaf exists in both workspaces (stable pane identity keys on the leaf/tile id, not the workspace). Uncontrolled focus and maximize are remembered PER WORKSPACE (_agent/workspace-set-concept.md §5.3): switching away and back restores that workspace's focused / maximized pane; a workspace never inherits the previous one's maximize. A drag in flight when the active workspace changes is cancelled. Pass focusedLeafId / maximizedLeafId to own that scoping yourself.

A TilingWorkspaceTabDragHover in externalDragHover settles the drag natively: on release the renderer applies moveLeafToWorkspace and reports the next set — no host onExternalDrop needed for tab drops.

Example

const [set, setSet] = useState<TilingWorkspaceSet>(() => workspaceSetOfLayout(initialLayout));
return (
  <TilingRenderer
    workspaces={set}
    onWorkspacesChange={setSet}
    tiles={tiles}
    config={DEFAULT_TILING_LAYOUT_CONFIG}
  />
);

Properties

Property

Modifiers

Type

Description

inactiveWorkspaces?

TilingInactiveWorkspacesMode

(Optional) Whether panes seated only in inactive workspaces stay mounted. Default "unmount": only the active tree's panes exist; switching away unmounts a pane (its host state is lost unless the tile is also seated in the new active tree — the stable pane pool keeps that instance). "keep-mounted": every pool tile seated anywhere in the set keeps its pane instance in the hidden stable pool while inactive, so host data hooks, subscriptions and scroll positions survive a round trip. Cost: hidden panes keep rendering and running effects; a tile is mounted lazily the first time its workspace is shown (never-shown tiles are not pre-mounted). Requires paneIdentity: "stable" (the "auto" default on a client mount); a "slot" renderer ignores it. Switching to an EMPTY workspace (layout: null) unmounts the tree — and with it the pool.

mintLeafId?

(tileId: string) => string

(Optional) Leaf id for a tile seated by orphanTiles: "seat-in-active". Default `` leaf:${tileId} ``. Must be unique across the whole set.

onIntegrityIssues?

(issues: ReadonlyArray<TilingWorkspaceSetIssue>) => void

(Optional) Set-wide integrity report (workspaceSetIssues(workspaces, { expectedTileIds: pool })). Fired after commit whenever the issue fingerprint changes — including the transition back to an empty list — under orphanTiles: "report" and "seat-in-active"; never under "ignore". Not fired on mount when the set is already clean.

onMoveLeaf?

(leafId: string, fromWorkspaceId: string, toWorkspaceId: string) => void

(Optional) Notified after a drag settled on a workspace tab and the leaf was moved (the moved set has already been reported through onWorkspacesChange). fromWorkspaceId is the workspace that was active when the drag started. With interaction.workspaces.followMovedLeaf: true the reported set is already switched to toWorkspaceId — do not switch again from here; a second commit would paint the leaf seated only in an inactive workspace first (a remount under inactiveWorkspaces: "unmount").

onWorkspacesChange

(workspaces: TilingWorkspaceSet) => void

Called with the next set whenever the renderer edits a tree or moves a leaf.

onWorkspaceSwitch?

(event: TilingWorkspaceSwitchEvent) => void

(Optional) Fired beside TilingRendererWorkspaceSetProps.onWorkspacesChange whenever activeId changes through the renderer. via is "key" for a keymap binding, "command" for an imperative dispatch, "reveal" for reveal-tile, "swipe" for a trackpad / touch swipe (interaction.workspaces.switch), "spring-load" for a spring-loaded tab drop (interaction.workspaces.springLoad; fired beside TilingRendererWorkspaceSetProps.onMoveLeaf), "tab-drop" for a plain tab release with followMovedLeaf: true (move + switch in one onWorkspacesChange; do not switch again from onMoveLeaf). "tab" is reserved for a host tab strip that routes through the same dispatch path (a strip that calls switch-workspace is reported as "command").

orphanTiles?

TilingOrphanTilePolicy

(Optional) What the renderer does with a pool tile that no workspace seats (and with a seated tile the pool no longer contains). In set mode TilingRendererCommonProps.tiles is the WHOLE tile pool and coverage is judged set-wide — the active tree only heals the tiles it seats itself, so a tile seated only in an inactive workspace is never pulled into the active tree. Default "report".

  • "report" — no mutation; the current issue list is delivered through TilingRendererWorkspaceSetProps.onIntegrityIssues whenever it changes. - "seat-in-active" — orphan tiles are seated in the active workspace (TILING_DEFAULT_WORKSPACE_PLACEMENT, leaf id from TilingRendererWorkspaceSetProps.mintLeafId) and unknown tiles are pruned; the repaired set is emitted once through onWorkspacesChange. Issues are still reported. - "ignore" — nothing happens; the set is rendered as given.

renderEmptyWorkspace?

(workspace: TilingWorkspace) => React.ReactNode

(Optional) Rendered inside the tiling root while the active workspace's layout is null (empty workspace). Undefined → an empty root element.

workspaceDefaults?

TilingWorkspaceSet

(Optional) Seed set for reset-workspace / reset-workspaces. Absent → those commands are no-ops (return false). Hosts that persist a visitor-edited set pass the original seed here so reset can restore one workspace or the whole set without switching activeId.

workspaces

TilingWorkspaceSet

The controlled workspace set. The activeId workspace's tree is rendered.

TilingRenderSurface

type

The renderer surface a renderTile invocation paints:

  • "pane" — the seated in-tree pane (the normal, interactive case). - "drag-ghost" — the floating, portaled pickup ghost that travels with the cursor during a drag (the overlay portal container, default document.body, see TilingRendererCommonProps.overlayPortalContainer). aria-hidden + pointer-events-none: handlers are inert no-ops, capability display flags stay real. - "drag-cancel" — the cancel fly-back overlay (the ghost gliding home after a cancelled drag; same overlay portal container as "drag-ghost"). Same inert-handlers / real-flags semantics as "drag-ghost".

Signature:

type TilingRenderSurface = "pane" | "drag-ghost" | "drag-cancel";

TilingRenderTileGroupContext

interface

Group context on TilingRenderTileProps.group: present when the pane being rendered is a tabbed group's ACTIVE member (the only member that renders, per the stacking contract), null for loose leaves and for the drag-ghost / drag-cancel surfaces. A custom pane uses it to paint its own group chrome (tabs, member list, eject/ungroup controls) instead of the built-in strip — typically with the strip suppressed via the grouping capability's showGroupTabStrip: false.

The callbacks route through the SAME internal command router the built-in strip and the keyboard layer use (group-tab-jump / remove-from-group / ungroup), so capability gating and observability stay uniform.

Signature:

interface TilingRenderTileGroupContext 

Properties

Property

Modifiers

Type

Description

activateMember

readonly

(memberNumber: number) => void

Activate the member at memberNumber (1-based; a TilingGroupMemberView.memberNumber) — dispatches group-tab-jump.

groupId

readonly

string

The group node id.

members

readonly

ReadonlyArray<TilingGroupMemberView>

The group's members in member order (see TilingGroupMemberView).

removeMember

readonly

(leafId: string) => void

Eject the member with leafId from the group (it re-seats as a loose pane) — dispatches remove-from-group.

ungroup

readonly

() => void

Dissolve the whole group into loose panes — dispatches ungroup.

TilingResizeCapability

type

Resize capability across the two divider orientations.

RESIZE AXIS CONVENTION (read carefully — the divider orientation and the resize axis are deliberately the opposite words):

  • A **vertical divider** sits between **side-by-side** panes. Dragging it changes their **widths** → this is a **horizontal** resize (x-axis). In the layout tree this is a split whose axis is "horizontal" (a flex-row container with a cursor-col-resize handle). - A **horizontal divider** sits between **stacked** panes. Dragging it changes their **heights** → this is a **vertical** resize (y-axis). In the layout tree this is a split whose axis is "vertical" (a flex-col container with a cursor-row-resize handle).

The resize-capability token therefore matches the split axis token directly: "horizontal" gates width dividers, "vertical" gates height dividers.

  • "both" — every divider is resizable (this is the "entire" option). Default. - "horizontal" — only width dividers (side-by-side panes) are resizable. - "vertical" — only height dividers (stacked panes) are resizable. - "none" — no divider is resizable.

Signature:

type TilingResizeCapability = "both" | "horizontal" | "vertical" | "none";

TilingResizeFloor

type

Which floor an interactive resize (pointer drag / keyboard) bounds a pane to (HT-RESIZE-FLOOR):

  • "chrome" (LIBRARY DEFAULT, HT-RESIZE-FLOOR-DEFAULT) — "size-out": the gutter may shrink all the way to the collapsed titlebar-only extent (TilingLayoutConfig.collapsedExtentPx), REPLACING the body floor for this leaf entirely (not layered on top of it). This is purely a resize-time ratio bound: it never touches collapsed / collapsedRestore (no auto-collapse, no state change from resize — only the titlebar-collapse control owns collapsed), and the pane stays flexible/ratio-based (re-grows with the window), never a sizing: "static" pin. Settle-on-release (snapping to a fixed size when released near the chrome floor) is intentionally NOT implemented. - "body" — the ordinary content floor: the min-pane precedence chain (TilingLeafNode.minBBoxPx → TilingSplitNode.minPaneSizePx → TilingLayoutConfig.minPaneSizePx). A resize cannot shrink the pane below its usable content size. Opt in per-leaf via TilingLeafNode.resizeFloor or library-wide via TilingLayoutConfig.resizeFloor for consumers that want the old content-floor behavior back.

Signature:

type TilingResizeFloor = "body" | "chrome";

TilingRevealTileChanged

type

What revealTile() changed: nothing, only the group tab, only the active workspace, or both.

Signature:

type TilingRevealTileChanged = "none" | "tab" | "workspace" | "both";

TilingRevealTileResult

interface

The result of revealTile() when the tile is seated in at least one workspace.

Signature:

interface TilingRevealTileResult 

Properties

Property

Modifiers

Type

Description

changed

readonly

TilingRevealTileChanged

Whether the active workspace and/or a group tab changed.

leafId

readonly

string

The leaf id bound to the tile (set-unique).

set

readonly

TilingWorkspaceSet

The set after the reveal (the input by reference when changed is "none").

workspaceId

readonly

TilingWorkspaceId

The workspace the tile was revealed in.

TilingSlotCommitmentCapability

interface

Live-drag slot-commitment configuration. mode selects the re-resolution policy; reresolveDeltaPx is the delta-responsive movement threshold (CSS px) — a coarse "should I re-aim" gate distinct from the fine 6px geometric zone hysteresis, so the two never double-count.

Signature:

interface TilingSlotCommitmentCapability 

Properties

Property

Modifiers

Type

Description

mode?

TilingSlotCommitmentMode

(Optional) Re-resolution policy after the ghost seats in a slot. Default "delta-responsive".

reresolveDeltaPx?

number

(Optional) delta-responsive re-aim threshold in CSS px. Default 24.

TilingSlotCommitmentMode

type

Live-drag slot re-resolution / commitment policy after the single ghost hops INTO and FILLS a resolved slot (see _agent/single-instance-hop-in-drag-design.md §8):

  • "zone-exit-hold" — anchored / sticky: the seated slot is pinned through small cursor movements and re-resolves only when the cursor crosses OUT of the seated target's hit footprint (high hysteresis). - "delta-responsive" — DEFAULT: the seated slot re-resolves eagerly once the cursor travels beyond reresolveDeltaPx from the seat anchor (or exits the footprint), so the user can re-aim without fully exiting the pane.

Only meaningful in dragMode: "live".

Signature:

type TilingSlotCommitmentMode = "zone-exit-hold" | "delta-responsive";

TilingSplitAxis

type

Orientation of a binary split. "horizontal" places its two children side-by-side (a flex-row, resized by a width divider); "vertical" stacks them (a flex-col, resized by a height divider).

Signature:

type TilingSplitAxis = "horizontal" | "vertical";

TilingSpringLoadCapability

type

The capability value interaction.workspaces.springLoad will accept once the renderer wiring lands: false (default — spring-load off) or a partial TilingSpringLoadConfig. Resolved by resolveSpringLoadCapability (./engine) to a config, or null when disabled.

Signature:

type TilingSpringLoadCapability = Partial<TilingSpringLoadConfig> | false;

References: TilingSpringLoadConfig

TilingSpringLoadConfig

interface

Tunables of the dwell FSM. Defaults in TILING_SPRING_LOAD_DEFAULTS.

Signature:

interface TilingSpringLoadConfig 

Properties

Property

Modifiers

Type

Description

dwellMs

number

How long (ms) the pointer must stay over the SAME non-active workspace tab, without releasing, before the drag commits into that workspace and re-arms there. Default 500.

TilingThemeDividerTokens

interface

Split-divider / gap handle chrome across visible + hidden states.

Signature:

interface TilingThemeDividerTokens 

Properties

Property

Modifiers

Type

Description

base

readonly

string

Structural base incl. focus-visible ring color. Include box-border + bg-clip-content so the renderer can pad the full gutter hit-target while chrome paints only the center handleSizePx content box.

hidden

readonly

string

Hidden handle (no chrome, hit-area only).

visibleInteractive

readonly

string

Visible + resizable handle (resting + hover).

visibleStatic

readonly

string

Visible but resize-disabled handle.

TilingThemeDragChromeTokens

interface

Drag-state chrome — how the rearrange drag LOOKS around the pane shell: the ghost wrapper's elevation delta, the content-less seat the ghost hops into, the picked-up source pane's dimming, the drop-target highlight, and the pointer-pinned drag-cursor badge. Every renderer-painted drag surface reads these tokens (never an inline class string), so a host can make the drag state read exactly like its at-rest pane — square, flat, hairline — without CSS overrides against [data-drag-ghost] / [data-drag-source-reservation].

TilingTheme.dragChrome is OPTIONAL and PARTIAL: resolveDragChrome(theme) fills every omitted token from a pane-shell-inheriting default derived from the theme itself (the seat wears paneShell.surface, the seat frame reuses resolveFocusFrame, the ghost carries a small neutral elevation/opacity delta). A theme that passes nothing therefore drags with its own resting chrome; the built-in neon-terminal look is expressed purely through this slot. Tokens stay literal class strings (Tailwind JIT) — that is not a portal constraint. Scoped host CSS vars still need overlayPortalContainer.

Signature:

interface TilingThemeDragChromeTokens 

Properties

Property

Modifiers

Type

Description

cancelFlyBack

readonly

string

Cancel fly-back wrapper (the pane gliding back to its origin).

cursorBadge

readonly

string

Drag-cursor badge (the pointer-pinned drop-validity affordance — insert / swap / invalid / grab glyph) shape + base: radius, border width, backdrop.

cursorBadgeInvalid

readonly

string

Cursor badge surface / border / glyph color over a blocked target.

cursorBadgeNeutral

readonly

string

Cursor badge surface / border / glyph color while free-following (grip).

cursorBadgeValid

readonly

string

Cursor badge surface / border / glyph color over a committable target.

dropIntentLayer

readonly

string

The default tile's drop-intent overlay FRAME (edge/center zone hints): its radius + border width. Colors come from the observability layer.

dropTarget

readonly

string

The resolved DROP-TARGET leaf wrapper. Default "": per the focus-follows-dragged-pane rule the destination is conveyed by the ghost hop-in, so no other pane is highlighted. A host may opt into a highlight.

ghostLifted

readonly

string

Ghost WRAPPER while the ghost free-follows the pointer (lifted): the elevation / scale / opacity / tint delta layered OUTSIDE the pane shell (theme.ghost.surface for the default tile, the host's own chrome for a custom renderTile). Keep it a small delta from the at-rest pane.

ghostSeated

readonly

string

Ghost wrapper while the ghost is SEATED in the hop-in slot (and under prefers-reduced-motion, where the lifted look is dropped).

ghostTransition

readonly

string

Ghost wrapper transition between the lifted and seated looks. Omitted entirely under prefers-reduced-motion.

resolveSeatFrame

readonly

(accent: TilingTileAccent | undefined) => string

The frame the seat wears during the hop-in flight, composed from the DRAGGED pane's accent (focus follows the dragged pane). Default: the theme's resolveFocusFrame. Return "" for a frameless seat.

sourcePane

readonly

string

The picked-up SOURCE pane as a whole (applied by the renderer to the leaf wrapper, so it dims a custom renderTile and the default tile alike): a single opacity, never a per-part (title-only) dim. Preview drag mode only — in live mode the source slot is the reservation.

sourceReservation

readonly

string

The content-less SEAT the single ghost hops into (DragSourceSlotReservation): the FULL surface class set — background, border, radius, shadow. Fully covered once the ghost seats, visible only during the hop-in flight. The default inherits paneShell.surface so the seat reads as the at-rest pane.

TilingThemeGhostTokens

interface

Drag-ghost shell — the lifted, portaled copy of the dragged pane.

Signature:

interface TilingThemeGhostTokens 

Properties

Property

Modifiers

Type

Description

bodyText

readonly

string

Ghost body text color.

header

readonly

string

Ghost header bar.

subtitleText

readonly

string

Ghost subtitle text color.

surface

readonly

string

Ghost article shell (a touch more opaque + deeper shadow than a pane).

TilingThemePaneHeaderTokens

interface

Pane header chrome — resting + focused + the per-pane control buttons.

Signature:

interface TilingThemePaneHeaderTokens 

Properties

Property

Modifiers

Type

Description

base

readonly

string

Resting header bar: border-b, bg, inset sheen.

controlActive

readonly

string

Active/pressed header control button.

controlIdle

readonly

string

Resting header control button (maximize etc.).

focused

readonly

string

Additive classes when the pane is focused.

selected

readonly

string

Additive header classes when the pane is part of the Alt/Opt+click multi-selection set. Deliberately NEUTRAL (no accent) so it never collides with the accent focus frame — multi-selection and focus are orthogonal states a pane can hold simultaneously.

selectedBadge

readonly

string

The small neutral "selected" check affordance rendered in the header of a multi-selected pane. Neutral tone, distinct from any accent control.

titleText

readonly

string

Pane title base typography (accent color applied separately).

TilingThemePaneShellTokens

interface

Pane host shell surfaces + interaction-state rings.

Signature:

interface TilingThemePaneShellTokens 

Properties

Property

Modifiers

Type

Description

bodyText

readonly

string

Pane body scroll region text color/leading.

invalidDropRing

readonly

string

Ring on an invalid drop target.

subtitleText

readonly

string

Pane subtitle text color.

surface

readonly

string

Pane article shell: bg/gradient, radius, shadow/rim, backdrop-filter.

TilingThemeRootTokens

interface

Renderer-root + viewport surfaces.

Gap flanks (divider margins / static spacers) and rounded-pane corner gutters show whatever paints here. Prefer bg-transparent so the host chrome shows through; set an opaque bg only when the tiling surface is a self-contained canvas (neon-terminal). Consumers can override via the theme prop without forking the library.

Signature:

interface TilingThemeRootTokens 

Properties

Property

Modifiers

Type

Description

container

readonly

string

Outer renderer container: bg/gradient, radius, padding, outline.

viewport

readonly

string

Inner viewport (where the pane tree lays out): bg + radius.

TilingThemeTopBarTokens

interface

Top-bar / tab-strip chrome.

Signature:

interface TilingThemeTopBarTokens 

Properties

Property

Modifiers

Type

Description

container

readonly

string

Tab-strip container: border, bg, shadow, backdrop.

controlGroup

readonly

string

Switcher-control group wrapper (theme picker etc.).

pickerGroup

readonly

string

Accent-picker group wrapper.

switcherCard

readonly

string

Centered pane-switcher overlay card.

switcherCardInactive

readonly

string

Switcher card when the pane is NOT selected.

tabBase

readonly

string

Tab chip base typography/layout.

tabInactive

readonly

string

Inactive tab chip.

titleText

readonly

string

Strip title text.

TilingThemeWorkspaceTransitionTokens

interface

Workspace-switch motion tokens (N2). Optional + partial — omitted fields resolve through resolveWorkspaceTransition() to the library defaults (200 ms, drag-hop easing). Same additive slot pattern as dragChrome / ghostChip.

Signature:

interface TilingThemeWorkspaceTransitionTokens 

Properties

Property

Modifiers

Type

Description

durationMs

readonly

number

Timed commit / cancel duration in milliseconds. Default 200.

easing

readonly

string

CSS <easing-function> for the timed curve.

TilingTileAccentSwatch

interface

A pickable accent paired with a human label and a solid Tailwind background class for rendering a swatch dot — the generic metadata a palette control (e.g. a host accent picker) iterates to offer accent selection.

Signature:

interface TilingTileAccentSwatch 

Properties

Property

Modifiers

Type

Description

accent

TilingTileAccent

The accent this swatch selects.

label

string

Human-readable label for the swatch (e.g. in a picker).

swatchClassName

string

Tailwind background class rendering the solid swatch dot.

TilingTitleBarSizingMode

type

The four title-bar sizing actions a pane offers for ITSELF (no target-pane indirection — the pane the control lives in is the target):

  • "flexible" — clear any pinned dimension and return the pane to ratio distribution (FLEX). - "static-height" — freeze the pane's height to its measured bbox px. - "static-width" — freeze the pane's width to its measured bbox px. - "static-both" — freeze both dimensions to the measured bbox.

Signature:

type TilingTitleBarSizingMode = "flexible" | "static-height" | "static-width" | "static-both";

TilingTouchDragCapability

interface

Touch-drag hardening configuration. The drag FSM runs on Pointer Events, so it already covers touch uniformly with mouse/pen — this capability tunes the touch-only choreography that must differ from mouse:

  • enable — when false, a touch press on the drag handle never starts a drag (touch is reserved for tap/scroll); mouse/pen drag is unaffected. Default true. - longPressMs — how long a finger must be held before the press becomes a drag pickup (the tap/scroll-vs-drag disambiguator). A pre-long-press scroll-axis flick releases to the page; mouse/pen skip this delay entirely. Default 220.

Signature:

interface TilingTouchDragCapability 

Properties

Property

Modifiers

Type

Description

enable?

boolean

(Optional) Allow touch pointers to start a drag. Default true.

longPressMs?

number

(Optional) Long-press delay (ms) before a held touch becomes a drag. Default 220.

TilingWorkspace

interface

One workspace: a stable id, a host-facing name, and its layout tree (null = empty).

Signature:

interface TilingWorkspace 

Properties

Property

Modifiers

Type

Description

id

readonly

TilingWorkspaceId

Unique within the set. Any non-empty string (e.g. "main", "ws:01H…").

layout

readonly

TilingLayoutNode | null

The workspace's tree, or null for an empty workspace.

name

readonly

string

Display name; 1..TILING_WORKSPACE_NAME_MAX_CHARS chars after trim.

TilingWorkspaceId

type

Host-minted workspace id — any non-empty string, unique within the set.

Signature:

type TilingWorkspaceId = string;

TilingWorkspacePanelElementProps

interface

Props UseTilingWorkspaceTabsResult.panelProps hands to a workspace's tabpanel.

Signature:

interface TilingWorkspacePanelElementProps 

Properties

Property

Modifiers

Type

Description

"aria-labelledby"

readonly

string

The owning tab's id.

hidden

readonly

boolean

true for every workspace but the active one.

id

readonly

string

${idPrefix}-panel-${workspaceId}.

role

readonly

"tabpanel"

WAI-ARIA role.

TilingWorkspacePlacement

type

Where a leaf is seated when it enters a tree — the placement vocabulary the workspace-set ops (moveLeafToWorkspace, showInWorkspace, orphan repair) and the engine's insertLeafInto share. Each variant maps onto one existing tree reducer's insert half:

  • root — a new root-level split on the current root's axis, the leaf on side (moveLeafToRoot). The one placement that exists in every tree; the default is { kind: "root", side: "second" } (reading order: last). - adjacent — beside targetLeafId on placement's side, wrapping both in a new split (insertLeafAdjacent). - split-container — into the existing split splitId on side (moveLeafToSplitContainer). - group — appended as the active member of group groupId (addLeafToGroup). - region — beside the reading-order first (start) or last (end) leaf of the destination tree: left of the first / right of the last, via the same path adjacent uses (insertLeafAdjacent). Reading order is the flatten queryWorkspaceSet(set).leafIds(workspaceId) returns (group members in tab order). When that neighbour is a group member, the group itself is the seat neighbour — wrap the group, do not enter it.

A placement whose target (leaf / split / group id) is absent from the destination tree falls back to { kind: "root", side: "second" }; a null destination becomes the bare leaf. TILING_DEFAULT_WORKSPACE_PLACEMENT stays { kind: "root", side: "second" }.

Signature:

type TilingWorkspacePlacement = {
    readonly kind: "root";
    readonly side: "first" | "second";
} | {
    readonly kind: "adjacent";
    readonly targetLeafId: string;
    readonly placement: TilingMovePlacement;
} | {
    readonly kind: "split-container";
    readonly splitId: string;
    readonly side: "first" | "second";
} | {
    readonly kind: "group";
    readonly groupId: string;
} | {
    readonly kind: "region";
    readonly region: "start" | "end";
};

References: TilingMovePlacement

TilingWorkspacesCapability

interface

Workspace-set navigation capability, the object form of interaction.workspaces. A bare boolean at the workspaces slot is shorthand for { enable }.

Signature:

interface TilingWorkspacesCapability 

Properties

Property

Modifiers

Type

Description

enable?

boolean

(Optional) Enable the workspace commands. When true (default) AND the renderer is in set mode (workspaces prop present), TilingCommandGates.workspacesEnabled is true. When false, those commands are no-ops even in set mode.

followMovedLeaf?

boolean

(Optional) Default follow for move-leaf-to-workspace when the command omits it. true moves activeId with the leaf (Hyprland movetoworkspace); false (default) leaves the current workspace active (movetoworkspacesilent).

springLoad?

TilingSpringLoadCapability | null

(Optional) Spring-loaded workspace-tab drop (N3): hold a dragged pane over another workspace's tab for dwellMs (default 500) and the pane commits into that workspace (moveLeafToWorkspace), the set switches there (onWorkspaceSwitch({ via: "spring-load" })) and the drag continues on the pane's new seat under the still-held pointer. false (default) disables it; an object enables it and overrides dwellMs. Needs a kind: "workspace-tab" external hover (resolveExternalDragHover / useTilingWorkspaceTabs, or the externalDragHover prop); the active workspace's own tab never dwells. null reads as false (so a resolved capability object re-resolves to itself).

switch?

TilingWorkspaceSwitchCapability

(Optional) Gesture switching between neighbouring workspaces (N1). Both inputs are opt-in (default false); see TilingWorkspaceSwitchCapability.

TilingWorkspaceSet

interface

The ordered collection of workspaces plus the active one — the value a set host owns in state and hands to TilingRenderer via the workspaces prop. Array order is tab order.

Signature:

interface TilingWorkspaceSet 

Properties

Property

Modifiers

Type

Description

activeId

readonly

TilingWorkspaceId

The rendered workspace. Always one of workspaces[].id.

workspaces

readonly

ReadonlyArray<TilingWorkspace>

Tab order. At least one workspace.

TilingWorkspaceSetCommitReason

type

Why useTilingWorkspaceSetController() handed a set to the host.

Signature:

type TilingWorkspaceSetCommitReason = "tree" | "lifecycle" | "move" | "reveal";

TilingWorkspaceSetController

interface

The controller useTilingWorkspaceSetController() returns. Pass set and onWorkspacesChange to <TilingRenderer> and useTilingWorkspaceTabs.

Signature:

interface TilingWorkspaceSetController 

Properties

Property

Modifiers

Type

Description

atDefaults

readonly

boolean

true when reset("all") would change nothing: every workspace matches defaults, regardless of which one is active. false when defaults is omitted.

create

readonly

() => TilingWorkspaceId | null

mint + createWorkspace + switch; null when capped / read-only / refused.

flush

readonly

() => void

Commit pending trees now (reason "tree").

moveTile

readonly

(tileId: string, to: TilingWorkspaceId, placement?: TilingWorkspacePlacement) => void

Move a tile to to (then switch to to). Commits "move". No-op when read-only.

onWorkspacesChange

readonly

(next: TilingWorkspaceSet) => void

Pass to <TilingRenderer onWorkspacesChange> and useTilingWorkspaceTabs. Diffs trees (debounced "tree" commit) vs activeId (lifecycle commit).

pending

readonly

boolean

true while trees are awaiting a "tree" commit.

remove

readonly

(id: TilingWorkspaceId) => ReadonlyArray<string>

Delete a workspace. Returns removedTileIds ([] when unchanged / read-only).

rename

readonly

(id: TilingWorkspaceId, name: string) => void

Rename a workspace. No-op when read-only or the engine refuses.

reset

readonly

(scope: "workspace" | "all", workspaceId?: string) => boolean

Restore the seed: "workspace" replaces one workspace (workspaceId omitted → the viewed activeId); "all" replaces the whole set while keeping the current activeId when that id exists in defaults. Immediate commit (reason "lifecycle"), not tree-debounced. false when there are no defaults, the host is read-only, or nothing changed.

reveal

readonly

(tileId: string) => TilingRevealTileResult | null

Reveal a tile (revealTile). Commits "reveal" when changed !== "none" and not read-only; read-only applies locally. null when the tile is seated nowhere.

set

readonly

TilingWorkspaceSet

The VIEWED set: value with locally kept trees and (read-only / optimistic) local active id merged over it. Pass to <TilingRenderer workspaces>.

switch

readonly

(id: TilingWorkspaceId) => void

Switch the viewed workspace. Local when readOnly, else commits "lifecycle".

TilingWorkspaceSetControllerOptions

interface

Options of useTilingWorkspaceSetController().

Signature:

interface TilingWorkspaceSetControllerOptions 

Properties

Property

Modifiers

Type

Description

defaults?

readonly

TilingWorkspaceSet

(Optional) Seed set for TilingWorkspaceSetController.reset and TilingWorkspaceSetController.atDefaults. Absent → reset returns false and atDefaults is false.

maxWorkspaces?

readonly

number

(Optional) Optional cap for create() (host concern). Default TILING_WORKSPACES_MAX.

mintWorkspaceId

readonly

() => string

Host policy for create(). Engine never names.

nextWorkspaceName

readonly

(existing: ReadonlyArray<TilingWorkspace>) => string

Host policy for create(). Receives the current workspaces (pending trees already folded).

onCommit

readonly

(next: TilingWorkspaceSet, reason: TilingWorkspaceSetCommitReason) => void

Receives the set the host should persist. reason tells why.

readOnly?

readonly

boolean

(Optional) When true: switches stay local, nothing commits (view-only board). Default false.

treeDebounceMs?

readonly

number

(Optional) Tree edits (divider, rearrange, tab activation, in-tree drop) coalesce for this long. Default 400.

value

readonly

TilingWorkspaceSet

Authoritative set from the host (e.g. its persisted document).

TilingWorkspaceSetIssue

interface

One integrity finding of workspaceSetIssues().

Signature:

interface TilingWorkspaceSetIssue 

Properties

Property

Modifiers

Type

Description

kind

readonly

TilingWorkspaceSetIssueKind

Which invariant is broken.

leafId?

readonly

string

(Optional) The leaf the finding is about, when leaf-local.

message

readonly

string

Human-readable summary.

tileId?

readonly

string

(Optional) The tile the finding is about, when tile-local.

workspaceId?

readonly

TilingWorkspaceId

(Optional) The workspace the finding is about, when it is workspace-local.

TilingWorkspaceSetIssueKind

type

The set-level invariants workspaceSetIssues() checks (per-tree structure is isStructurallyValidLayout):

  • no-workspaces — the set is empty. - empty-workspace-id / duplicate-workspace-id — workspace ids are non-empty and unique. - invalid-workspace-name — a name that fails normalizeWorkspaceName(). - active-workspace-missing — activeId names no workspace. - invalid-tree — a tree fails isStructurallyValidLayout (duplicate leaf or tile inside one tree, empty ids, bad split). - leaf-tile-binding-mismatch — a leaf id bound to a different tile id (or a tile id to a different leaf id) than in an earlier workspace; a leaf id resolves to ONE logical pane across the set. - orphan-tile — an expectedTileIds member seated in no workspace. - unknown-tile — a seated tile absent from expectedTileIds.

Signature:

type TilingWorkspaceSetIssueKind = "no-workspaces" | "empty-workspace-id" | "duplicate-workspace-id" | "invalid-workspace-name" | "active-workspace-missing" | "invalid-tree" | "leaf-tile-binding-mismatch" | "orphan-tile" | "unknown-tile";

TilingWorkspaceSetQuery

interface

Read-only view over a set — the set-level analogue of queryTilingLayout.

Signature:

interface TilingWorkspaceSetQuery 

Properties

Property

Modifiers

Type

Description

active

readonly

TilingWorkspace

The active workspace (the first one when activeId is dangling).

leafCount

readonly

(workspaceId: TilingWorkspaceId) => number

Number of leaves a workspace shows (0 for an empty one).

leafIds

readonly

(workspaceId: TilingWorkspaceId) => ReadonlyArray<string>

Every leaf id of one workspace in reading order (group members in tab order).

neighbour

readonly

(workspaceId: TilingWorkspaceId, direction: TilingPaneCycleDirection) => TilingWorkspaceId | null

The workspace after / before workspaceId in tab order (wraps); null with one workspace.

tileIds

readonly

(workspaceId: TilingWorkspaceId) => ReadonlyArray<string>

Every tile id of one workspace in reading order.

workspace

readonly

(id: TilingWorkspaceId) => TilingWorkspace | null

Look a workspace up by id.

workspacesOfLeaf

readonly

(leafId: string) => ReadonlyArray<TilingWorkspaceId>

Ids of the workspaces showing leafId, in tab order.

workspacesOfTile

readonly

(tileId: string) => ReadonlyArray<TilingWorkspaceId>

Ids of the workspaces showing tileId, in tab order.

TilingWorkspaceSetRepairReason

type

What repairWorkspaceSet() did, in application order:

  • workspace-created — empty set → one main / "Main" workspace. - workspace-dropped — a workspace with an empty or duplicate id was removed; leaves shown only there moved to the kept workspace. - workspace-renamed — an invalid name was replaced. - tree-rebuilt — a structurally invalid tree was rebuilt from its distinct leaves in reading order. - leaf-rebound — a leaf whose id / tile binding disagreed with an earlier seat was rewritten to the canonical id. - leaf-removed — a mismatched seat that could not be rebound was removed. - unknown-tile-pruned — a seat of a tile outside expectedTileIds was removed. - orphan-seated — an expected tile shown nowhere was seated. - active-reset — a dangling activeId now names the first workspace.

Signature:

type TilingWorkspaceSetRepairReason = "workspace-created" | "workspace-dropped" | "workspace-renamed" | "tree-rebuilt" | "leaf-rebound" | "leaf-removed" | "unknown-tile-pruned" | "orphan-seated" | "active-reset";

TilingWorkspaceSetRepairResult

interface

The result of repairWorkspaceSet().

Signature:

interface TilingWorkspaceSetRepairResult 

Properties

Property

Modifiers

Type

Description

reasons

readonly

ReadonlyArray<TilingWorkspaceSetRepairReason>

Every rule that fired, in application order, one entry per fix.

set

readonly

TilingWorkspaceSet

The repaired set (the input by reference when nothing was wrong).

TilingWorkspaceSwipeConfig

interface

Tunables of the swipe FSM. Every field has a default in TILING_WORKSPACE_SWIPE_DEFAULTS; interaction.workspaces.switch.wheelSwipe accepts a partial override.

Signature:

interface TilingWorkspaceSwipeConfig 

Properties

Property

Modifiers

Type

Description

commitFraction

number

|progress| (travel / viewport width) at gesture end at or above which the swipe commits. Default 0.35.

commitVelocityPxMs

number

Peak sample velocity (CSS px per ms, measured towards the target) at or above which the swipe commits regardless of distance — the flick. Default 0.6.

lockoutMs

number

After a commit every WHEEL sample is swallowed for this long so trackpad momentum cannot start a second gesture. Default 350.

modifier

TilingWorkspaceSwipeModifier

Keyboard modifier that must be held on the arming samples of a wheel run ("meta" / "alt" / "shift"). Default null — no modifier required. Once tracking, releasing the modifier does not cancel or convert the gesture to scroll. Touch ignores this. ctrlKey stays a pinch-zoom reject regardless.

thresholdPx

number

Horizontal travel (CSS px, summed wheel dx or finger displacement) an armed gesture must reach before it becomes tracking. Below it a gesture is jitter and never owns the input. Default 24.

wheelIdleMs

number

A wheel gesture ends when no WHEEL sample arrived for this long; the host's IDLE_TICK carries the clock reading the reducer compares against the last sample. Default 120.

widthPx

number

Fallback viewport width (CSS px) used to normalise travel into progress when neither SET_CONTEXT nor the sample supplied one. The wrapper always supplies the measured width; the default only keeps the machine total. Default 800.

wrap

boolean

Whether a swipe past the first / last workspace wraps around. false (default) keeps the machine idle for a direction with no neighbour — no rubber-band in this release.

TilingWorkspaceSwipeModifier

type

Keyboard modifier a wheel swipe must be held with on its arming samples. null (default) requires none — today's behaviour. Touch swipes ignore this (no keyboard). ctrl is never a swipe modifier: ctrlKey is always a pinch-zoom reject.

Signature:

type TilingWorkspaceSwipeModifier = "meta" | "alt" | "shift" | null;

TilingWorkspaceSwipePhase

type

The lifecycle phase of the swipe FSM state (TilingWorkspaceSwipeState).

Signature:

type TilingWorkspaceSwipePhase = "idle" | "armed" | "tracking" | "settling" | "lockout";

TilingWorkspaceSwipeScope()

function

Shares one swipe store between a set-mode TilingRenderer and host chrome (a tab strip) so useWorkspaceSwipe() outside the renderer follows the gesture. Optional: without it the renderer keeps a private store.

Signature:

declare function TilingWorkspaceSwipeScope(input: {
    children?: React.ReactNode;
}): React.ReactElement;

Parameters

Parameter

Type

Description

{ children, }

(not declared)

input

{ children?: React.ReactNode; }

Returns:

React.ReactElement

TilingWorkspaceSwipeSnapshot

interface

The host-facing projection of the FSM state a tab strip indicator reads.

Signature:

interface TilingWorkspaceSwipeSnapshot 

Properties

Property

Modifiers

Type

Description

phase

TilingWorkspaceSwipePhase

The lifecycle phase.

progress

number

Signed progress -1..1; negative = towards prev.

target

TilingWorkspaceSwipeTarget | null

The side the swipe heads to; null when idle or cancelled.

TilingWorkspaceSwipeTarget

type

Which side of the active workspace a swipe is heading to.

Signature:

type TilingWorkspaceSwipeTarget = "prev" | "next";

TilingWorkspaceSwitchCapability

interface

Gesture switching, the switch slot of interaction.workspaces. A swipe ends as a cycle-workspace command through the ordinary dispatch path and reports onWorkspaceSwitch({ via: "swipe" }). While either input is on the renderer root carries overscroll-behavior-x: contain; with touchSwipe also touch-action: pan-y.

Signature:

interface TilingWorkspaceSwitchCapability 

Properties

Property

Modifiers

Type

Description

touchSwipe?

boolean

(Optional) One-finger horizontal pan on the viewport. Default false.

transition?

TilingWorkspaceTransitionMode

(Optional) Visual transition played by the set-mode renderer on every activeId change (tab, keymap, dispatch, reveal-tile, swipe, or a host that sets workspaces.activeId directly): "slide" (outgoing clone slides out while the incoming tree slides in), "fade" (cross-fade), or "none" (default — no stage is mounted, the tree swaps in place). While a swipe is tracking the stage follows the finger (progress scrub) and settles with the swipe's commit / cancel. Downgraded to "none" under prefers-reduced-motion: reduce and when the outgoing view cannot be captured (see resolveTransitionMode).

wheelSwipe?

boolean | Partial<TilingWorkspaceSwipeConfig>

(Optional) Trackpad / horizontal-wheel swipe. true uses TILING_WORKSPACE_SWIPE_DEFAULTS; an object overrides individual TilingWorkspaceSwipeConfig fields (shared with touch), including optional modifier. Default false.

TilingWorkspaceSwitchEvent

interface

Semantic event beside onWorkspacesChange when activeId changes.

Signature:

interface TilingWorkspaceSwitchEvent 

Properties

Property

Modifiers

Type

Description

from

string

Workspace id that was active before the switch.

to

string

Workspace id that is active after the switch.

via

TilingWorkspaceSwitchVia

Which renderer surface initiated the switch.

TilingWorkspaceSwitchVia

type

How a workspace switch was initiated through the renderer. "swipe" is a trackpad / touch swipe (interaction.workspaces.switch). "spring-load" is the spring-loaded tab drop (interaction.workspaces.springLoad): the dwell elapsed, the dragged leaf moved into the hovered workspace and the set switched there. "tab-drop" is a plain (non-spring-loaded) drag release on a workspace tab with interaction.workspaces.followMovedLeaf: true: the leaf moved and the set switched in the same onWorkspacesChange. "tab" is reserved for a host tab strip that routes through the same dispatch path.

Signature:

type TilingWorkspaceSwitchVia = "tab" | "tab-drop" | "key" | "command" | "swipe" | "spring-load" | "reveal";

TilingWorkspaceTab

interface

One workspace as the strip presents it.

Signature:

interface TilingWorkspaceTab 

Properties

Property

Modifiers

Type

Description

activate

readonly

() => void

Switch to this workspace.

close

readonly

() => TilingDeleteWorkspaceResult

Delete this workspace (refused for the last one). Returns the engine result.

id

readonly

string

The tab element's DOM id.

index

readonly

number

Position in the strip.

isActive

readonly

boolean

Is this the set's active workspace.

isDropTarget

readonly

boolean

Is a dragged leaf hovering this tab (the renderer's resolved workspace-tab hover).

isFocused

readonly

boolean

Does this tab hold the roving focus (the one tabIndex: 0 tab).

rename

readonly

(name: string) => boolean

Rename; false when the engine refused (empty / over-long / unchanged name).

requestClose

readonly

() => void

Fire the host's close affordance (onCloseRequest).

requestRename

readonly

() => void

Fire the host's rename affordance (onRenameRequest).

tabProps

readonly

TilingWorkspaceTabElementProps

Spread onto the tab element (a <button>).

workspace

readonly

TilingWorkspace

The workspace this tab presents.

TilingWorkspaceTabDragHover

interface

"Pointer is over a WORKSPACE TAB" — the engine-settled external target. In workspace-set mode the renderer applies moveLeafToWorkspace(leafId, workspaceId, placement) on release and reports the next set through onWorkspacesChange (claim-before-settle: no cancel fly-back, no host hit-test). In single-layout mode it behaves like TilingExternalDropHover — the host's onExternalDrop claims. targetId is the tab's own id (what the ghostChip slot receives).

Signature:

interface TilingWorkspaceTabDragHover 

Properties

Property

Modifiers

Type

Description

kind

readonly

"workspace-tab"

Discriminant.

placement?

readonly

TilingWorkspacePlacement

(Optional) Where in the destination tree the leaf lands. Undefined → root, second side.

point

readonly

TilingClientPoint

Pointer position over the tab (window-client CSS px).

targetId

readonly

string

The tab's own id (what the ghostChip slot receives).

workspaceId

readonly

string

The workspace the tab stands for — the moveLeafToWorkspace destination.

TilingWorkspaceTabElementProps

interface

Props TilingWorkspaceTab.tabProps hands to the tab element.

Signature:

interface TilingWorkspaceTabElementProps 

Properties

Property

Modifiers

Type

Description

"aria-controls"

readonly

string

The matching tabpanel id.

"aria-selected"

readonly

boolean

true on the set's active workspace.

"data-active"

readonly

"" | undefined

Present ("") on the active tab.

"data-drop-target"

readonly

"" | undefined

Present ("") while a dragged leaf hovers this tab.

"data-workspace-id"

readonly

string

The workspace id, for styling hooks and tests.

className

readonly

string | undefined

Theme workspaceTab (+ workspaceTabActive / workspaceTabDropTarget) tokens; undefined when unstyled.

id

readonly

string

${idPrefix}-tab-${workspaceId} — also the drop target's targetId.

onClick

readonly

() => void

Activates the workspace and takes the roving focus.

onFocus

readonly

() => void

Takes the roving focus.

onKeyDown

readonly

(event: React.KeyboardEvent<HTMLElement>) => void

The keyboard model (arrows / Home / End / Enter / Space / F2 / Delete).

ref

readonly

(element: HTMLElement | null) => void

Registers the element for focus management and drop hit-testing.

role

readonly

"tab"

WAI-ARIA role.

tabIndex

readonly

0 | -1

Roving focus: 0 on the focused tab, -1 elsewhere.

type

readonly

"button"

Keeps a <button> out of form submission.

TilingWorkspaceTabKeyAction

type

What a key press on a focused workspace tab asks for. focus moves the roving focus (arrows wrap; Home / End jump); activate switches to the focused workspace (Enter / Space); rename (F2) and close (Delete) are affordance requests the host answers. null — not a tab-strip key.

Signature:

type TilingWorkspaceTabKeyAction = {
    readonly kind: "focus";
    readonly index: number;
} | {
    readonly kind: "activate";
} | {
    readonly kind: "rename";
} | {
    readonly kind: "close";
};

TilingWorkspaceTabs()

function

Render-prop form of useTilingWorkspaceTabs(). Paints nothing itself.

Signature:

declare function TilingWorkspaceTabs(input: TilingWorkspaceTabsProps): React.ReactElement;

Parameters

Parameter

Type

Description

{ children, ...options }

(not declared)

input

TilingWorkspaceTabsProps

Returns:

React.ReactElement

Example

<TilingWorkspaceTabs workspaces={set} onWorkspacesChange={setSet}>
  {({ tabs, tablistProps }) => (
    <div {...tablistProps}>
      {tabs.map((tab) => (
        <button key={tab.workspace.id} {...tab.tabProps}>{tab.workspace.name}</button>
      ))}
    </div>
  )}
</TilingWorkspaceTabs>

TilingWorkspaceTabsOrientation

type

Which axis the tab strip lays out along; picks the arrow keys that move focus.

Signature:

type TilingWorkspaceTabsOrientation = "horizontal" | "vertical";

TilingWorkspaceTabTarget

interface

One workspace tab registered as a drop target: its id, workspace and measured rect.

Signature:

interface TilingWorkspaceTabTarget 

Properties

Property

Modifiers

Type

Description

placement?

readonly

TilingWorkspacePlacement

(Optional) Where a dropped leaf lands in that workspace. Undefined → root, second side.

rect

readonly

TilingClientRect

Where the tab sits, in window-client coordinates.

targetId

readonly

string

The tab's own id — what TilingExternalDragHover.targetId / the ghostChip slot receive.

workspaceId

readonly

string

The workspace the tab stands for (the moveLeafToWorkspace destination).

TilingWorkspaceTransitionConfig

interface

Duration / easing / default mode for a workspace switch. Matches the planned interaction.workspaces.switch.transition capability plus the theme workspaceTransition tokens.

Signature:

interface TilingWorkspaceTransitionConfig 

Properties

Property

Modifiers

Type

Description

durationMs

readonly

number

Timed commit / cancel duration in milliseconds. Default 200.

easing

readonly

string

CSS <easing-function> for the timed curve (theme / host).

mode

readonly

TilingWorkspaceTransitionMode

Paint mode. Default "none" until a host (or the set-mode wrapper follow-up) opts into "slide" / "fade".

TilingWorkspaceTransitionDirection

type

Tab-order direction of a workspace switch.

"next" slides the clone toward −100 % (incoming from +100 %); "prev" slides the clone toward +100 % (incoming from −100 %).

Signature:

type TilingWorkspaceTransitionDirection = "prev" | "next";

TilingWorkspaceTransitionLayerStyle

interface

Compositor-friendly transform + opacity pair for one side of the stage.

Signature:

interface TilingWorkspaceTransitionLayerStyle 

Properties

Property

Modifiers

Type

Description

opacity

readonly

number

Opacity in [0, 1].

transform

readonly

string

transform value (translateX(...) or none).

TilingWorkspaceTransitionMode

type

How a workspace switch is painted.

  • "none" — instant; no clone, no motion (default, and the reduced-motion resolution). - "slide" — outgoing clone and live incoming tree translateX in opposite directions. - "fade" — outgoing clone opacity 1 → 0 over the live incoming tree.

Signature:

type TilingWorkspaceTransitionMode = "none" | "slide" | "fade";

TilingWorkspaceTreeDiff

interface

One workspace whose layout reference differs between two sets.

Signature:

interface TilingWorkspaceTreeDiff 

Properties

Property

Modifiers

Type

Description

layout

readonly

TilingLayoutNode | null

The incoming tree (null = empty workspace).

workspaceId

readonly

TilingWorkspaceId

Workspace whose tree changed.

TilingWorkspaceTreeMap

type

Workspace id → tree overlay (local edits or trees awaiting a commit).

Signature:

type TilingWorkspaceTreeMap = ReadonlyMap<TilingWorkspaceId, TilingLayoutNode | null>;

References: TilingWorkspaceId, TilingLayoutNode

TouchInputSample

interface

One touch position sample (TOUCH_START / TOUCH_MOVE).

Signature:

interface TouchInputSample 

Properties

Property

Modifiers

Type

Description

canScrollFurther

boolean

The scroll chain under the TOUCH-START target can still scroll on X in the finger's direction (judged per move; false on the start sample).

ts

number

Monotonic clock reading (ms).

widthPx

number | null

Viewport width (CSS px) at the sample; null when the host has no laid-out width.

x

number

Finger client X.

y

number

Finger client Y.

transitionTransform()

function

Transform / opacity for the outgoing clone and the live incoming tree.

progress is unit (0 = outgoing fully covering, 1 = incoming fully covering). Slide uses translateX only (compositor-friendly). Fade keeps both transforms at none and drops outgoing opacity 1 → 0 (incoming stays 1 — the live tree sits underneath). "none" is identity on both sides.

Signature:

declare function transitionTransform(progress: number, direction: TilingWorkspaceTransitionDirection, mode: TilingWorkspaceTransitionMode): TilingWorkspaceTransitionTransform;

Parameters

Parameter

Type

Description

progress

number

direction

TilingWorkspaceTransitionDirection

mode

TilingWorkspaceTransitionMode

Returns:

TilingWorkspaceTransitionTransform

unitProgressFromSigned()

function

Convert N1 swipe progress (−1..1) into unit progress (0..1) for direction. Positive signed progress is "next"; negative is "prev". The opposite sign clamps to 0.

Signature:

declare function unitProgressFromSigned(signed: number, direction: TilingWorkspaceTransitionDirection): number;

Parameters

Parameter

Type

Description

signed

number

direction

TilingWorkspaceTransitionDirection

Returns:

number

useTilingWorkspaceSetController()

function

Controlled workspace-set session: the host owns the persisted document (value / onCommit); the controller owns the in-flight tree overlays, the debounce timer, and the read-only local active id.

Every mutation first folds pending trees into the base so a lifecycle commit never drops a tree edit made a moment earlier. Same-reference engine results are ignored (no commit). Unmount with pending trees flushes synchronously via onCommit.

Signature:

declare function useTilingWorkspaceSetController(options: TilingWorkspaceSetControllerOptions): TilingWorkspaceSetController;

Parameters

Parameter

Type

Description

options

TilingWorkspaceSetControllerOptions

Returns:

TilingWorkspaceSetController

Example

const ctl = useTilingWorkspaceSetController({
  value: persisted,
  onCommit: (next, reason) => persist(next, reason),
  readOnly,
  mintWorkspaceId: () => crypto.randomUUID(),
  nextWorkspaceName: (existing) => `Workspace ${existing.length + 1}`,
});
const tabs = useTilingWorkspaceTabs({
  workspaces: ctl.set,
  onWorkspacesChange: ctl.onWorkspacesChange,
});
return (
  <TilingRenderer
    workspaces={ctl.set}
    onWorkspacesChange={ctl.onWorkspacesChange}
    tiles={tiles}
    {...tabs.rendererProps}
  />
);

useTilingWorkspaceTabs()

function

Headless workspace tab strip over a controlled TilingWorkspaceSet. Returns per-tab props (role="tab", aria-selected, roving tabIndex, keyboard handling), tablist / tabpanel props, rename / close affordances, and the renderer props that make every tab a native drop target. Renders nothing itself.

Signature:

declare function useTilingWorkspaceTabs(options: UseTilingWorkspaceTabsOptions): UseTilingWorkspaceTabsResult;

Parameters

Parameter

Type

Description

options

UseTilingWorkspaceTabsOptions

Returns:

UseTilingWorkspaceTabsResult

UseTilingWorkspaceTabsOptions

interface

Options of useTilingWorkspaceTabs().

Signature:

interface UseTilingWorkspaceTabsOptions 

Properties

Property

Modifiers

Type

Description

activation?

readonly

"manual" | "automatic"

(Optional) "manual" (default, WAI-ARIA recommended for tabs that swap heavy content): arrows move focus, Enter / Space activates. "automatic": moving focus also activates.

dropPlacement?

readonly

(workspace: TilingWorkspace) => TilingWorkspacePlacement | undefined

(Optional) Where a leaf dropped on a workspace's tab lands in that workspace. Undefined → the engine default (root, second side).

idPrefix?

readonly

string

(Optional) Prefix for the generated DOM ids: tabs are ${idPrefix}-tab-${workspaceId}, panels ${idPrefix}-panel-${workspaceId}. Default "hpt-workspace".

onCloseRequest?

readonly

(workspace: TilingWorkspace) => void

(Optional) The close affordance: Delete on a focused tab (or tab.requestClose()) asks the host to confirm; the host commits with tab.close(). Absent → Delete is not handled.

onRenameRequest?

readonly

(workspace: TilingWorkspace) => void

(Optional) The rename affordance: F2 on a focused tab (or tab.requestRename()) asks the host to open its editor; the host commits with tab.rename(name). Absent → F2 is not handled.

onWorkspacesChange

readonly

(workspaces: TilingWorkspaceSet) => void

Receives the next set for every edit the strip performs (switch, rename, close, create).

orientation?

readonly

TilingWorkspaceTabsOrientation

(Optional) Arrow keys that move focus. Default "horizontal" (Left / Right).

theme?

readonly

TilingTheme

(Optional) Theme whose workspaceTab* tokens class the strip. Default: the TilingThemeProvider context, else the library default theme.

workspaces

readonly

TilingWorkspaceSet

The controlled workspace set the strip presents (the same object the renderer gets).

UseTilingWorkspaceTabsResult

interface

What useTilingWorkspaceTabs() returns.

Signature:

interface UseTilingWorkspaceTabsResult 

Properties

Property

Modifiers

Type

Description

activeTab

readonly

TilingWorkspaceTab | null

The set's active tab (null only for a dangling activeId).

create

readonly

(input: CreateWorkspaceInput) => boolean

Create a workspace (createWorkspace); false when the engine refused.

dropTargetWorkspaceId

readonly

string | null

The workspace whose tab a dragged leaf is hovering, else null.

focusTab

readonly

(workspaceId: string) => void

Move the roving focus to a tab (and focus its element).

panelProps

readonly

(workspaceId: string) => TilingWorkspacePanelElementProps

Props for the tabpanel presenting workspaceId (the renderer's container, typically).

rendererProps

readonly

{ readonly resolveExternalDragHover: TilingExternalDragHoverResolver; readonly onExternalDragHoverChange: (hover: TilingExternalDragHover | null) => void; }

Spread onto TilingRenderer: the tab hit-test as resolveExternalDragHover plus onExternalDragHoverChange feeding isDropTarget back. No host hit-test, no host claim.

tablistProps

readonly

TilingWorkspaceTablistElementProps

Spread onto the tablist element.

tabs

readonly

ReadonlyArray<TilingWorkspaceTab>

One entry per workspace, in set order.

useWorkspaceSwipe()

function

Headless read of the workspace swipe in progress: { progress, target, phase }. progress is -1..1 (negative = towards the previous workspace), target the side the swipe heads to, phase the FSM phase. Live only under a TilingWorkspaceSwipeScope() that also contains the set-mode TilingRenderer; elsewhere it is the idle snapshot. Use it to move a tab indicator in step with the gesture.

Signature:

declare function useWorkspaceSwipe(): TilingWorkspaceSwipeSnapshot;

Returns:

TilingWorkspaceSwipeSnapshot

useWorkspaceTransition()

function

Imperative workspace-switch transition. begin freezes the outgoing viewport; scrub follows N1 swipe progress; finish runs the timed commit / cancel curve and drops the clone.

Signature:

declare function useWorkspaceTransition(options: UseWorkspaceTransitionOptions): UseWorkspaceTransitionResult;

Parameters

Parameter

Type

Description

options

UseWorkspaceTransitionOptions

Returns:

UseWorkspaceTransitionResult

UseWorkspaceTransitionOptions

interface

Options of useWorkspaceTransition().

viewportRef is the set-mode viewport root (position: relative; overflow: hidden). begin clones that node.

Signature:

interface UseWorkspaceTransitionOptions 

Properties

Property

Modifiers

Type

Description

durationMs?

readonly

number

(Optional) Timed commit / cancel duration. Default theme token or 200.

easing?

readonly

string

(Optional) CSS easing token (stored for hosts; the rAF path uses ease-out cubic).

mode?

readonly

TilingWorkspaceTransitionMode

(Optional) Requested paint mode. Resolved against reduced-motion and capture degradation. Default "none".

onSettled?

readonly

(kind: WorkspaceTransitionSettleKind) => void

(Optional) Fired after the clone is removed.

reducedMotion?

readonly

boolean

(Optional) Override prefers-reduced-motion. When omitted the hook reads matchMedia("(prefers-reduced-motion: reduce)").

viewportRef

readonly

React.RefObject<HTMLElement | null>

Viewport whose outgoing tree is cloned on begin.

UseWorkspaceTransitionResult

interface

Imperative handle returned by useWorkspaceTransition().

Signature:

interface UseWorkspaceTransitionResult 

Properties

Property

Modifiers

Type

Description

activeMode

readonly

TilingWorkspaceTransitionMode

Resolved mode of the in-flight transition ("none" when idle).

begin

readonly

(input: { direction: TilingWorkspaceTransitionDirection; mode?: TilingWorkspaceTransitionMode; }) => void

Capture the current viewport (outgoing) and arm the stage. Call **before** the live tree switches to the incoming workspace. A second begin mid-flight replaces the clone with a fresh capture.

direction

readonly

TilingWorkspaceTransitionDirection | null

Direction of the in-flight transition (null when idle).

finish

readonly

(kind: WorkspaceTransitionSettleKind) => void

Run the timed curve to the end (commit, unit progress → 1) or back to the start (cancel, → 0), then fire onSettled and drop the clone.

incomingStyle

readonly

React.CSSProperties

Style for the live incoming wrapper (transform + opacity).

outgoingNode

readonly

HTMLElement | null

Frozen clone (null when idle / "none" / after settle).

outgoingStyle

readonly

React.CSSProperties

Style for the clone overlay (transform + opacity).

phase

readonly

WorkspaceTransitionPhase

Current phase.

scrub

readonly

(progress: number) => void

Scrub with N1 swipe progress (−1..1). No timer while progress is being fed. No-op until begin.

unitProgress

readonly

number

Unit progress in [0, 1].

VIEW_CAPTURE_CANVAS_PIXEL_BUDGET

variable

Pixel-area budget for a single canvas copy (width * height). Above this the capture is marked degraded so the caller falls back from "slide" to "fade" (a 16 megapixel bitmap is too large to snapshot synchronously).

Signature:

VIEW_CAPTURE_CANVAS_PIXEL_BUDGET: number

viewedWorkspaceSet()

function

The set a renderer / tab strip should see: value with local tree overlays and an optional local activeId (read-only view switching, or an optimistic switch that has not landed in value yet). Same reference when nothing overlays.

Signature:

declare function viewedWorkspaceSet(value: TilingWorkspaceSet, localTrees: TilingWorkspaceTreeMap, localActiveId?: TilingWorkspaceId | null): TilingWorkspaceSet;

Parameters

Parameter

Type

Description

value

TilingWorkspaceSet

localTrees

TilingWorkspaceTreeMap

localActiveId

TilingWorkspaceId | null

(Optional)

Returns:

TilingWorkspaceSet

WheelInputSample

interface

One wheel sample, in the FSM's WHEEL event shape plus the port's annotations.

Signature:

interface WheelInputSample 

Properties

Property

Modifiers

Type

Description

altKey

boolean

WheelEvent.altKey at the sample (swipe modifier gate).

canScrollFurther

boolean

The scroll chain under the event target can still scroll on X in dx's direction.

ctrlKey

boolean

ctrlKey (trackpad pinch-zoom reports as a ctrl-wheel).

dx

number

Horizontal scroll delta (CSS px, deltaMode already normalised to pixels).

dy

number

Vertical scroll delta (CSS px).

metaKey

boolean

WheelEvent.metaKey at the sample (swipe modifier gate).

shiftKey

boolean

WheelEvent.shiftKey at the sample (swipe modifier gate).

ts

number

Monotonic clock reading (ms).

widthPx

number | null

Viewport width (CSS px) at the sample; null when the host has no laid-out width.

WheelTouchInputPort

interface

The wheel / touch subscription the swipe host runs the FSM on.

  • subscribe attaches the listeners (idempotent per listener) and returns the detach function. - setTracking(true) tells the port the FSM owns the gesture: the adapter may now cancel the browser default (a non-passive wheel listener that calls preventDefault, stopping history back-swipe). Outside tracking the listeners are passive so the page scrolls normally.

Signature:

interface WheelTouchInputPort 

Methods

Method

Description

setTracking(tracking)

The FSM is (true) / is not (false) tracking a gesture — toggles the non-passive preventDefault policy.

subscribe(listener)

Attach listener; returns its detach function. Listeners are attached lazily on the first subscriber.

WORKSPACE_KEY_BINDINGS

variable

Optional workspace-navigation key-binding fragment. Hosts merge it into interaction.keyBindings.bindings — it is **not** part of defaultKeyBindings / TILING_KEYMAP_DEFAULTS (no default chords; hosts opt in). switch-workspace uses 1-based index (and) move-leaf-to-workspace uses direction) so the fragment does not need a live workspace set at construction time; the set-mode wrapper resolves those against the current tab order at dispatch.

  • Alt+ArrowLeft / Alt+ArrowRight → cycle-workspace previous / next - Alt+Digit1..Alt+Digit9 → switch-workspace to the n-th workspace - Alt+Shift+ArrowLeft / Alt+Shift+ArrowRight → move-leaf-to-workspace previous / next with follow: true

Signature:

WORKSPACE_KEY_BINDINGS: ReadonlyArray<TilingKeyBinding>

workspaceSetEquals()

function

Structural equality of two workspace sets: tab-order ids, names, activeId, and every persisted layout-tree field (leaf ids / tile ids / sizing / collapse / min-bbox, split axis / ratio / children / master flags, group ids / members / active tab). Same-reference inputs are equal.

Signature:

declare function workspaceSetEquals(a: TilingWorkspaceSet, b: TilingWorkspaceSet): boolean;

Parameters

Parameter

Type

Description

a

TilingWorkspaceSet

b

TilingWorkspaceSet

Returns:

boolean

WorkspaceSetIntegrityOptions

interface

Options for workspaceSetIssues() / repairWorkspaceSet().

Signature:

interface WorkspaceSetIntegrityOptions 

Properties

Property

Modifiers

Type

Description

expectedTileIds?

readonly

ReadonlyArray<string>

(Optional) The host's tile registry, set-wide. When given, every expected tile must be seated in ≥ 1 workspace (orphan-tile) and no tree may seat an unknown tile (unknown-tile). Omit to check structure only.

workspaceSetOfLayout()

function

Single-tree sugar: the one-workspace set a layout / onLayoutChange host is projected onto ({ workspaces: [{ id, name, layout }], activeId: id }).

Signature:

declare function workspaceSetOfLayout(layout: TilingLayoutNode | null, id?: TilingWorkspaceId, name?: string): TilingWorkspaceSet;

Parameters

Parameter

Type

Description

layout

TilingLayoutNode | null

id

TilingWorkspaceId

(Optional)

name

string

(Optional)

Returns:

TilingWorkspaceSet

workspaceSetsAlign()

function

true when both sets have the same tab order, names, layout refs, and activeId.

Signature:

declare function workspaceSetsAlign(left: TilingWorkspaceSet, right: TilingWorkspaceSet): boolean;

Parameters

Parameter

Type

Description

left

TilingWorkspaceSet

right

TilingWorkspaceSet

Returns:

boolean

WorkspaceTransitionPhase

type

Phase of useWorkspaceTransition().

Signature:

type WorkspaceTransitionPhase = "idle" | "scrubbing" | "settling";

WorkspaceTransitionSettleKind

type

Why the stage released the clone.

Signature:

type WorkspaceTransitionSettleKind = "commit" | "cancel";

WorkspaceTransitionStage

variable

Visual workspace-switch stage: an absolutely positioned overlay (the frozen outgoing clone) over the live incoming tree. Drive it through the imperative ref (begin / scrub / finish) or feed N1 progress.

Signature:

WorkspaceTransitionStage: React.ForwardRefExoticComponent<WorkspaceTransitionStageProps & React.RefAttributes<UseWorkspaceTransitionResult>>

WorkspaceTransitionStageProps

interface

Props of WorkspaceTransitionStage. The stage wraps the live incoming tree and paints the clone as an absolutely positioned overlay. Imperative control is the ref (UseWorkspaceTransitionResult); progress is the N1 swipe feed (non-null → scrub).

Signature:

interface WorkspaceTransitionStageProps 

Properties

Property

Modifiers

Type

Description

children?

readonly

React.ReactNode

(Optional) Live incoming tree.

className?

readonly

string

(Optional) Optional class on the stage root.

durationMs?

readonly

number

(Optional) Timed duration. Default theme / 200 ms.

easing?

readonly

string

(Optional) CSS easing token. Default theme / drag-hop easing.

mode?

readonly

TilingWorkspaceTransitionMode

(Optional) Requested paint mode. Default "none".

onSettled?

readonly

(kind: WorkspaceTransitionSettleKind) => void

(Optional) Fired after the clone is removed.

progress?

readonly

number | null

(Optional) Signed swipe progress (−1..1). While non-null the stage is scrubbed (no timer). finish is still explicit.

reducedMotion?

readonly

boolean

(Optional) Override prefers-reduced-motion (tests / hosts that already track it).

viewportRef

readonly

React.RefObject<HTMLElement | null>

Viewport cloned on begin. Typically an ancestor of this stage.