{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "window-manager",
  "title": "Window Manager",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A desktop of real windows: focus raises, the title bar drags, edges and corners resize, double-click zooms, the yellow light tucks a window into a tray, and Alt plus arrow keys move and resize from the keyboard.",
  "dependencies": [
    "motion@^12"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/mac-window.json",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/window-manager.tsx",
      "content": "// Ballmac UI: Window Manager. https://ui.ballmac.com/components/window-manager\n\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\n\nimport { MacWindow, MacWindowContent, MacWindowTitleBar } from \"@/components/ballmac/mac-window\"\nimport { cn } from \"@/lib/utils\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\n\ntype ManagedWindow = {\n  /** Unique id. */\n  id: string\n  /** Title shown in the title bar and the tray. */\n  title: string\n  /** What the window shows. */\n  content: React.ReactNode\n  /** Left edge in pixels. Windows without a position cascade. */\n  x?: number\n  /** Top edge in pixels. */\n  y?: number\n  /** Width in pixels. */\n  width?: number\n  /** Height in pixels. */\n  height?: number\n  /** Smallest width it can be resized to. */\n  minWidth?: number\n  /** Smallest height it can be resized to. */\n  minHeight?: number\n}\n\ntype Rect = { x: number; y: number; w: number; h: number }\ntype WinState = Rect & { id: string; z: number; minimized: boolean; maximized: boolean; restore?: Rect }\n\ntype WindowManagerProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** The windows. Add an entry to open one; remove it to close it. */\n  windows: ManagedWindow[]\n  /** Called when a window's close button is pressed. Remove the window from `windows` to actually close it. */\n  onClose?: (id: string) => void\n  /** Content behind the windows, such as desktop icons or a wallpaper. */\n  children?: React.ReactNode\n  /** Accessible name of the desktop. */\n  label?: string\n  /** Accessible name of the strip that holds minimized windows. */\n  trayLabel?: string\n}\n\nconst EDGES = [\"n\", \"s\", \"e\", \"w\", \"ne\", \"nw\", \"se\", \"sw\"] as const\nconst EDGE_STYLE: Record<(typeof EDGES)[number], string> = {\n  n: \"top-[-3px] start-3 end-3 h-1.5 cursor-ns-resize\",\n  s: \"bottom-[-3px] start-3 end-3 h-1.5 cursor-ns-resize\",\n  e: \"end-[-3px] top-3 bottom-3 w-1.5 cursor-ew-resize\",\n  w: \"start-[-3px] top-3 bottom-3 w-1.5 cursor-ew-resize\",\n  ne: \"top-[-4px] end-[-4px] size-3.5 cursor-nesw-resize\",\n  nw: \"top-[-4px] start-[-4px] size-3.5 cursor-nwse-resize\",\n  se: \"bottom-[-4px] end-[-4px] size-3.5 cursor-nwse-resize\",\n  sw: \"bottom-[-4px] start-[-4px] size-3.5 cursor-nesw-resize\",\n}\n\nfunction initial(w: ManagedWindow, index: number, z: number): WinState {\n  return { id: w.id, x: w.x ?? 28 + index * 36, y: w.y ?? 24 + index * 30, w: w.width ?? 380, h: w.height ?? 250, z, minimized: false, maximized: false }\n}\n\n/**\n * A desktop of real, draggable windows: focus raises a window, the title bar drags, edges and corners resize,\n * double-clicking the title bar zooms, the yellow light tucks a window into the tray.\n * With a window focused, Alt plus an arrow key moves it (Shift for bigger steps), Alt+Shift+arrows resize it.\n */\nfunction WindowManager({ windows, onClose, children, label, trayLabel, className, ...props }: WindowManagerProps) {\n  const msg = useMessages()\n  label ??= msg(\"window-manager.label\", \"Desktop\")\n  trayLabel ??= msg(\"window-manager.trayLabel\", \"Minimized windows\")\n  const reduce = useReducedMotion()\n  const deskRef = React.useRef<HTMLDivElement>(null)\n  const [states, setStates] = React.useState<WinState[]>(() => windows.map((w, i) => initial(w, i, i + 1)))\n  const [dragging, setDragging] = React.useState<string | null>(null)\n  const top = React.useRef(windows.length)\n  const gesture = React.useRef<{ id: string; mode: \"move\" | \"resize\"; edge?: string; px: number; py: number; start: Rect } | null>(null)\n\n  // Open windows that were added, and drop the ones that were removed.\n  React.useEffect(() => {\n    setStates((current) => {\n      const known = new Set(current.map((s) => s.id))\n      const added = windows.filter((w) => !known.has(w.id)).map((w, i) => initial(w, current.length + i, ++top.current))\n      const kept = current.filter((s) => windows.some((w) => w.id === s.id))\n      return added.length || kept.length !== current.length ? [...kept, ...added] : current\n    })\n  }, [windows])\n\n  const count = states.length\n  // Keep every window inside the desktop when it opens or when the desktop shrinks.\n  React.useEffect(() => {\n    const el = deskRef.current\n    if (!el) return\n    const fit = () => {\n      const dw = el.clientWidth\n      const dh = el.clientHeight\n      if (!dw || !dh) return\n      setStates((all) => {\n        let changed = false\n        const next = all.map((s) => {\n          if (s.maximized) return s\n          const w = Math.min(s.w, Math.max(160, dw - 16))\n          const h = Math.min(s.h, Math.max(110, dh - 16))\n          const x = Math.max(8, Math.min(s.x, dw - w - 8))\n          const y = Math.max(0, Math.min(s.y, dh - h - 8))\n          if (w === s.w && h === s.h && x === s.x && y === s.y) return s\n          changed = true\n          return { ...s, w, h, x, y }\n        })\n        return changed ? next : all\n      })\n    }\n    fit()\n    const ro = new ResizeObserver(fit)\n    ro.observe(el)\n    return () => ro.disconnect()\n  }, [count])\n\n  const patch = React.useCallback((id: string, change: Partial<WinState> | ((s: WinState) => Partial<WinState>)) => {\n    setStates((all) => all.map((s) => (s.id === id ? { ...s, ...(typeof change === \"function\" ? change(s) : change) } : s)))\n  }, [])\n\n  const raise = React.useCallback((id: string) => {\n    setStates((all) => {\n      const me = all.find((s) => s.id === id)\n      if (!me || (me.z === top.current && !me.minimized)) return all\n      return all.map((s) => (s.id === id ? { ...s, z: ++top.current, minimized: false } : s))\n    })\n  }, [])\n\n  const desk = () => ({ w: deskRef.current?.clientWidth ?? 800, h: deskRef.current?.clientHeight ?? 500 })\n  const spec = (id: string) => windows.find((w) => w.id === id)\n\n  function begin(e: React.PointerEvent, s: WinState, mode: \"move\" | \"resize\", edge?: string) {\n    if (s.maximized && mode === \"resize\") return\n    gesture.current = { id: s.id, mode, edge, px: e.clientX, py: e.clientY, start: { x: s.x, y: s.y, w: s.w, h: s.h } }\n    setDragging(s.id)\n    ;(e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId)\n  }\n\n  function onMove(e: React.PointerEvent) {\n    const g = gesture.current\n    if (!g) return\n    const dx = e.clientX - g.px\n    const dy = e.clientY - g.py\n    const { w: dw, h: dh } = desk()\n    if (g.mode === \"move\") {\n      patch(g.id, { maximized: false, x: Math.min(dw - 80, Math.max(80 - g.start.w, g.start.x + dx)), y: Math.min(dh - 32, Math.max(0, g.start.y + dy)) })\n      return\n    }\n    const win = spec(g.id)\n    const minW = win?.minWidth ?? 220\n    const minH = win?.minHeight ?? 140\n    let { x, y, w, h } = g.start\n    const edge = g.edge ?? \"\"\n    if (edge.includes(\"e\")) w = Math.max(minW, g.start.w + dx)\n    if (edge.includes(\"s\")) h = Math.max(minH, g.start.h + dy)\n    if (edge.includes(\"w\")) {\n      w = Math.max(minW, g.start.w - dx)\n      x = g.start.x + (g.start.w - w)\n    }\n    if (edge.includes(\"n\")) {\n      h = Math.max(minH, g.start.h - dy)\n      y = Math.max(0, g.start.y + (g.start.h - h))\n      h = g.start.h + (g.start.y - y)\n    }\n    patch(g.id, { x, y, w, h })\n  }\n\n  function end() {\n    gesture.current = null\n    setDragging(null)\n  }\n\n  function zoom(s: WinState) {\n    if (s.maximized) patch(s.id, { maximized: false, ...(s.restore ?? {}) })\n    else patch(s.id, { maximized: true, restore: { x: s.x, y: s.y, w: s.w, h: s.h } })\n    raise(s.id)\n  }\n\n  function onKeyDown(e: React.KeyboardEvent, s: WinState) {\n    if (e.target !== e.currentTarget) return\n    const mod = e.metaKey || e.ctrlKey\n    if (mod && e.key.toLowerCase() === \"w\") {\n      e.preventDefault()\n      onClose?.(s.id)\n      return\n    }\n    if (mod && e.key.toLowerCase() === \"m\") {\n      e.preventDefault()\n      patch(s.id, { minimized: true })\n      return\n    }\n    if (!e.altKey) return\n    const dirs: Record<string, [number, number]> = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] }\n    const d = dirs[e.key]\n    if (!d) return\n    e.preventDefault()\n    const win = spec(s.id)\n    const step = 16\n    const { w: dw, h: dh } = desk()\n    if (e.shiftKey) {\n      patch(s.id, { maximized: false, w: Math.max(win?.minWidth ?? 220, s.w + d[0] * step), h: Math.max(win?.minHeight ?? 140, s.h + d[1] * step) })\n    } else {\n      patch(s.id, { maximized: false, x: Math.min(dw - 80, Math.max(80 - s.w, s.x + d[0] * step)), y: Math.min(dh - 32, Math.max(0, s.y + d[1] * step)) })\n    }\n  }\n\n  const visible = states.filter((s) => !s.minimized)\n  const activeId = visible.reduce<WinState | null>((best, s) => (!best || s.z > best.z ? s : best), null)?.id\n  const tray = states.filter((s) => s.minimized)\n\n  return (\n    <div\n      ref={deskRef}\n      data-slot=\"window-manager\"\n      role=\"group\"\n      aria-label={label}\n      onPointerMove={onMove}\n      onPointerUp={end}\n      onPointerCancel={end}\n      className={cn(\"relative isolate overflow-hidden [--mac-accent:oklch(0.53_0.2_258)]\", className)}\n      {...props}\n    >\n      {children}\n      <AnimatePresence>\n        {states.map((s) => {\n          const win = spec(s.id)\n          if (!win) return null\n          const { w: dw, h: dh } = desk()\n          const rect = s.maximized ? { x: 0, y: 0, w: dw, h: dh } : s\n          return (\n            <motion.div\n              key={s.id}\n              data-slot=\"managed-window\"\n              role=\"group\"\n              aria-label={msg(\"window-manager.windowAltAndArrowKeys\", \"{title} window. Alt and arrow keys move it; Alt, Shift and arrow keys resize it.\", { title: win.title })}\n              tabIndex={s.minimized ? -1 : 0}\n              {...(s.minimized ? { inert: true } : {})}\n              onPointerDownCapture={() => raise(s.id)}\n              onFocusCapture={() => raise(s.id)}\n              onKeyDown={(e) => onKeyDown(e, s)}\n              initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.92 }}\n              animate={s.minimized ? (reduce ? { opacity: 0 } : { opacity: 0, scale: 0.7, y: dh - s.y }) : { opacity: 1, scale: 1, y: 0 }}\n              exit={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.92 }}\n              transition={{ duration: reduce ? 0.1 : 0.22, ease: [0.22, 1, 0.36, 1] }}\n              className={cn(\"absolute rounded-[14px] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/60\", dragging !== s.id && \"[transition:left_0.2s,top_0.2s,width_0.2s,height_0.2s] motion-reduce:transition-none\", s.minimized && \"pointer-events-none\")}\n              style={{ left: rect.x, top: rect.y, width: rect.w, height: rect.h, zIndex: s.z }}\n            >\n              <MacWindow\n                active={activeId === s.id}\n                className=\"size-full\"\n                onClose={() => onClose?.(s.id)}\n                onMinimize={() => patch(s.id, { minimized: true })}\n                onZoom={() => zoom(s)}\n              >\n                <MacWindowTitleBar\n                  title={win.title}\n                  className=\"cursor-grab touch-none active:cursor-grabbing\"\n                  onPointerDown={(e) => {\n                    if ((e.target as HTMLElement).closest(\"button\")) return\n                    begin(e, s, \"move\")\n                  }}\n                  onDoubleClick={(e) => {\n                    if (!(e.target as HTMLElement).closest(\"button\")) zoom(s)\n                  }}\n                />\n                <MacWindowContent>{win.content}</MacWindowContent>\n              </MacWindow>\n              {!s.maximized &&\n                EDGES.map((edge) => (\n                  <span key={edge} aria-hidden=\"true\" onPointerDown={(e) => begin(e, s, \"resize\", edge)} className={cn(\"absolute z-10 touch-none\", EDGE_STYLE[edge])} />\n                ))}\n            </motion.div>\n          )\n        })}\n      </AnimatePresence>\n      {tray.length > 0 && (\n        <ul aria-label={trayLabel} className=\"absolute bottom-3 left-1/2 z-[999] flex max-w-[92%] -translate-x-1/2 gap-1.5 rounded-2xl border border-white/20 bg-black/35 p-1.5 shadow-lg backdrop-blur-xl\">\n          {tray.map((s) => (\n            <li key={s.id}>\n              <button\n                type=\"button\"\n                aria-label={msg(\"window-manager.restore\", \"Restore {title}\", { title: spec(s.id)?.title ?? s.id })}\n                onClick={() => raise(s.id)}\n                className=\"flex h-8 max-w-40 items-center rounded-xl bg-white/15 px-3 text-[12px] font-medium text-white outline-none transition-colors hover:bg-white/25 focus-visible:ring-[3px] focus-visible:ring-white/60\"\n              >\n                <span className=\"truncate\">{spec(s.id)?.title}</span>\n              </button>\n            </li>\n          ))}\n        </ul>\n      )}\n    </div>\n  )\n}\n\nexport { WindowManager, type WindowManagerProps, type ManagedWindow }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/window-manager.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "window manager",
      "drag",
      "resize",
      "desktop",
      "macos"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "<WindowManager windows={[{ id, title, content, x, y, width, height, minWidth, minHeight }]} onClose>background</WindowManager>. Add or remove entries to open or close windows. Children render behind the windows.",
      "whenToUse": [
        "Interactive OS-style demos",
        "Multi-panel tools that need freely arranged windows"
      ],
      "whenNotToUse": [
        "Fixed split layouts (resizable)",
        "Dialogs (dialog)"
      ],
      "composesWith": [
        "mac-window",
        "desktop-icons",
        "dock",
        "sheet-dialog"
      ],
      "a11y": [
        {
          "keys": "Alt+Arrow",
          "action": "Moves the focused window"
        },
        {
          "keys": "Alt+Shift+Arrow",
          "action": "Resizes it"
        },
        {
          "keys": "Ctrl/Cmd+W and M",
          "action": "Close and minimize"
        },
        {
          "keys": "Screen readers",
          "action": "Each window is a labelled group; minimized windows are restorable from a named list"
        },
        {
          "keys": "Reduced motion",
          "action": "Windows appear and minimize without scaling"
        }
      ],
      "customization": [
        "windows",
        "onClose",
        "minWidth and minHeight",
        "children as the desktop"
      ]
    },
    "examples": [
      "window-manager-demo",
      "window-manager-controlled"
    ],
    "url": "https://ui.ballmac.com/components/window-manager"
  },
  "categories": [
    "macos"
  ],
  "type": "registry:ui"
}