{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "app-window",
  "title": "App Window",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "The window chrome of a real desktop app (Tauri or Electron): a title bar that drags the window, double-click to maximize, and the controls the platform draws: traffic lights, Windows caption buttons, or none.",
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/i18n.json",
    "https://ui.ballmac.com/r/feel-presets.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/app-window.tsx",
      "content": "// Ballmac UI: App Window. https://ui.ballmac.com/components/app-window\n// Based on tauri-controls (MIT, Copyright (c) 2023 agmmnn): window control layout and metrics, rewritten without any Tauri dependency.\n\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\nimport { useFeel, type FeelId } from \"@/lib/ballmac/feel\"\n\ntype AppWindowContextValue = {\n  feel: FeelId\n  active: boolean\n  maximized: boolean\n  onClose?: () => void\n  onMinimize?: () => void\n  onMaximize?: () => void\n  onFullscreen?: () => void\n}\n\nconst AppWindowContext = React.createContext<AppWindowContextValue>({ feel: \"web\", active: true, maximized: false })\n\n// Tauri reads `data-tauri-drag-region`; Electron reads the CSS property. Both are set, so the same bar drags in either.\nconst drag = { WebkitAppRegion: \"drag\" } as React.CSSProperties\nconst noDrag = { WebkitAppRegion: \"no-drag\" } as React.CSSProperties\n\nconst subscribeFocus = (cb: () => void) => {\n  window.addEventListener(\"focus\", cb)\n  window.addEventListener(\"blur\", cb)\n  return () => {\n    window.removeEventListener(\"focus\", cb)\n    window.removeEventListener(\"blur\", cb)\n  }\n}\n\n/** Whether this window has focus, so a window in the background dims as it does on the desktop. */\nfunction useWindowFocus() {\n  return React.useSyncExternalStore(subscribeFocus, () => document.hasFocus(), () => true)\n}\n\ntype AppWindowProps = React.ComponentProps<\"div\"> & {\n  /** Which platform's chrome to draw. Defaults to the page's feel (`data-feel`), which Feel Auto sets from the device. */\n  feel?: FeelId\n  /** Whether the window is the focused one. Defaults to following the real window focus. */\n  active?: boolean\n  /** The window fills the screen: corners square off and the Windows button shows \"restore\". */\n  maximized?: boolean\n  /** Called by the close button. Without a handler the button is drawn but not focusable. */\n  onClose?: () => void\n  /** Called by the minimize button. */\n  onMinimize?: () => void\n  /** Called by the maximize (Windows) or zoom (macOS) button, and when the title bar is double-clicked. */\n  onMaximize?: () => void\n  /** Called by the macOS green button; hold Option to call onMaximize instead. Falls back to onMaximize. */\n  onFullscreen?: () => void\n}\n\n/**\n * The frame of a desktop app window: a title bar that drags the window and carries the controls the platform draws (traffic\n * lights on the left for macOS, minimize, maximize and close on the right for Windows, none for iOS and web), and the content.\n * It draws chrome only; you wire the callbacks to Tauri's `getCurrentWindow()` or Electron's IPC.\n */\nfunction AppWindow({ feel: feelProp, active, maximized = false, onClose, onMinimize, onMaximize, onFullscreen, className, ...props }: AppWindowProps) {\n  const page = useFeel().feel\n  const focused = useWindowFocus()\n  const feel = feelProp ?? page\n  const isActive = active ?? focused\n  const value = React.useMemo(\n    () => ({ feel, active: isActive, maximized, onClose, onMinimize, onMaximize, onFullscreen }),\n    [feel, isActive, maximized, onClose, onMinimize, onMaximize, onFullscreen]\n  )\n  return (\n    <AppWindowContext.Provider value={value}>\n      <div\n        data-slot=\"app-window\"\n        data-feel={feel}\n        data-state={isActive ? \"active\" : \"inactive\"}\n        data-maximized={maximized || undefined}\n        className={cn(\n          \"group/app-window relative isolate flex min-h-0 min-w-0 flex-col overflow-hidden bg-card text-card-foreground\",\n          maximized ? \"rounded-none\" : \"rounded-xl border border-border\",\n          className\n        )}\n        {...props}\n      />\n    </AppWindowContext.Provider>\n  )\n}\n\nconst lights = {\n  close: <path d=\"M3.5 3.5l5 5m0-5l-5 5\" strokeWidth=\"1.3\" strokeLinecap=\"round\" />,\n  minimize: <path d=\"M3 6h6\" strokeWidth=\"1.4\" strokeLinecap=\"round\" />,\n  zoom: <path d=\"M3.4 8.6V5l3.6 3.6zM8.6 3.4V7L5 3.4z\" stroke=\"none\" fill=\"currentColor\" />,\n  plus: <path d=\"M6 3v6M3 6h6\" strokeWidth=\"1.3\" strokeLinecap=\"round\" />,\n} as const\n\nfunction TrafficLight({ kind, glyph, label, onPress }: { kind: \"close\" | \"minimize\" | \"zoom\"; glyph: keyof typeof lights; label: string; onPress?: () => void }) {\n  const shared = cn(\n    \"relative flex size-[12px] shrink-0 items-center justify-center rounded-full text-black/60 outline-none\",\n    \"bg-(--light) shadow-[inset_0_0_0_0.5px_rgb(0_0_0/0.18)] dark:shadow-[inset_0_0_0_0.5px_rgb(255_255_255/0.12)]\",\n    \"group-data-[state=inactive]/app-window:bg-foreground/15 group-data-[state=inactive]/app-window:group-hover/lights:bg-(--light)\"\n  )\n  const style = { \"--light\": `var(--mac-${kind}, ${kind === \"close\" ? \"oklch(0.7 0.19 25)\" : kind === \"minimize\" ? \"oklch(0.84 0.16 84)\" : \"oklch(0.76 0.19 145)\"})`, ...noDrag } as React.CSSProperties\n  const icon = (\n    <svg viewBox=\"0 0 12 12\" aria-hidden=\"true\" stroke=\"currentColor\" fill=\"none\" className=\"size-3 opacity-0 transition-opacity duration-100 group-hover/lights:opacity-100 group-has-[:focus-visible]/lights:opacity-100\">\n      {lights[glyph]}\n    </svg>\n  )\n  if (!onPress) return <span data-slot={`app-window-${kind}`} className={shared} style={style} aria-hidden=\"true\">{icon}</span>\n  return (\n    <button type=\"button\" data-slot={`app-window-${kind}`} aria-label={label} onClick={onPress} className={cn(shared, \"focus-visible:ring-[3px] focus-visible:ring-ring/50\")} style={style}>\n      {icon}\n    </button>\n  )\n}\n\nfunction MacControls() {\n  const { onClose, onMinimize, onMaximize, onFullscreen } = React.useContext(AppWindowContext)\n  const msg = useMessages()\n  // Holding Option turns the green button from full screen into a plain zoom, as it does on macOS.\n  const [alt, setAlt] = React.useState(false)\n  React.useEffect(() => {\n    const set = (e: KeyboardEvent) => setAlt(e.altKey)\n    window.addEventListener(\"keydown\", set)\n    window.addEventListener(\"keyup\", set)\n    return () => {\n      window.removeEventListener(\"keydown\", set)\n      window.removeEventListener(\"keyup\", set)\n    }\n  }, [])\n  const zoom = alt && onFullscreen ? onMaximize : (onFullscreen ?? onMaximize)\n  // 12 px lights 12 px apart keep every target 24 px from its neighbours, which is what WCAG 2.5.8 asks of small targets\n  return (\n    <div data-slot=\"app-window-controls\" dir=\"ltr\" className=\"group/lights flex shrink-0 items-center gap-[12px]\" style={noDrag}>\n      <TrafficLight kind=\"close\" glyph=\"close\" label={msg(\"app-window.close\", \"Close window\")} onPress={onClose} />\n      <TrafficLight kind=\"minimize\" glyph=\"minimize\" label={msg(\"app-window.minimize\", \"Minimize window\")} onPress={onMinimize} />\n      <TrafficLight kind=\"zoom\" glyph={alt ? \"plus\" : \"zoom\"} label={alt ? msg(\"app-window.zoom\", \"Zoom window\") : msg(\"app-window.fullscreen\", \"Enter full screen\")} onPress={zoom} />\n    </div>\n  )\n}\n\nconst caption = {\n  minimize: <path d=\"M0 5h10\" />,\n  maximize: <rect x=\"0.5\" y=\"0.5\" width=\"9\" height=\"9\" rx=\"1\" />,\n  restore: (\n    <>\n      <path d=\"M2.5 2.5V1.5a1 1 0 0 1 1-1h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-1\" />\n      <rect x=\"0.5\" y=\"2.5\" width=\"7\" height=\"7\" rx=\"1\" />\n    </>\n  ),\n  close: <path d=\"M0.5 0.5l9 9m0-9l-9 9\" />,\n} as const\n\nfunction CaptionButton({ kind, label, onPress }: { kind: keyof typeof caption; label: string; onPress?: () => void }) {\n  return (\n    <button\n      type=\"button\"\n      data-slot={`app-window-${kind}`}\n      aria-label={label}\n      aria-disabled={onPress ? undefined : true}\n      tabIndex={onPress ? undefined : -1}\n      onClick={onPress}\n      style={noDrag}\n      className={cn(\n        \"flex h-full w-[46px] cursor-default items-center justify-center rounded-none text-foreground outline-none transition-colors duration-100 focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/50\",\n        \"group-data-[state=inactive]/app-window:text-muted-foreground\",\n        kind === \"close\" ? \"hover:bg-destructive hover:text-white active:bg-destructive/90\" : \"hover:bg-foreground/[0.06] active:bg-foreground/[0.04]\"\n      )}\n    >\n      <svg viewBox=\"0 0 10 10\" width=\"10\" height=\"10\" aria-hidden=\"true\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1\">\n        {caption[kind]}\n      </svg>\n    </button>\n  )\n}\n\nfunction WindowsControls() {\n  const { maximized, onClose, onMinimize, onMaximize } = React.useContext(AppWindowContext)\n  const msg = useMessages()\n  return (\n    <div data-slot=\"app-window-controls\" dir=\"ltr\" className=\"flex h-full shrink-0 items-stretch\">\n      <CaptionButton kind=\"minimize\" label={msg(\"app-window.minimize\", \"Minimize window\")} onPress={onMinimize} />\n      <CaptionButton kind={maximized ? \"restore\" : \"maximize\"} label={maximized ? msg(\"app-window.restore\", \"Restore window\") : msg(\"app-window.maximize\", \"Maximize window\")} onPress={onMaximize} />\n      <CaptionButton kind=\"close\" label={msg(\"app-window.close\", \"Close window\")} onPress={onClose} />\n    </div>\n  )\n}\n\ntype AppWindowControlsProps = { feel?: FeelId }\n\n/** Just the window controls the platform draws, for a custom title bar. Draws nothing for iOS and web. */\nfunction AppWindowControls({ feel: feelProp }: AppWindowControlsProps) {\n  const { feel: ctx } = React.useContext(AppWindowContext)\n  const feel = feelProp ?? ctx\n  if (feel === \"macos\") return <MacControls />\n  if (feel === \"windows\") return <WindowsControls />\n  return null\n}\n\ntype AppWindowTitleBarProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n  /** The window title. Centered on macOS and iOS, at the start beside the icon on Windows. */\n  title?: React.ReactNode\n  /** A small icon before the title (Windows). */\n  icon?: React.ReactNode\n  /** Items after the controls and title, at the start (a back button, a sidebar toggle). */\n  leading?: React.ReactNode\n  /** Draw the window controls. Turn off when something else draws them. */\n  controls?: boolean\n}\n\n/**\n * The title bar. It drags the window (Tauri and Electron), maximizes on double-click, and sizes itself from the feel\n * (`--bm-titlebar-h`). Children become the trailing toolbar; buttons inside it stay clickable.\n */\nfunction AppWindowTitleBar({ title, icon, leading, controls = true, className, children, onDoubleClick, ...props }: AppWindowTitleBarProps) {\n  const { feel, onMaximize } = React.useContext(AppWindowContext)\n  const mac = feel === \"macos\"\n  const win = feel === \"windows\"\n  return (\n    <div\n      data-slot=\"app-window-titlebar\"\n      data-tauri-drag-region=\"\"\n      style={drag}\n      onDoubleClick={(e) => {\n        onDoubleClick?.(e)\n        // only the bar itself and its title, never a button or field inside it\n        if (!e.defaultPrevented && onMaximize && (e.target === e.currentTarget || (e.target as Element).closest(\"[data-slot=app-window-title]\"))) onMaximize()\n      }}\n      className={cn(\n        \"relative flex h-[var(--bm-titlebar-h,2.5rem)] shrink-0 items-center gap-2 border-b border-border bg-card select-none\",\n        win ? \"ps-3\" : \"px-3\",\n        className\n      )}\n      {...props}\n    >\n      {controls && mac ? <AppWindowControls /> : null}\n      {win && icon ? <span data-slot=\"app-window-icon\" className=\"flex size-4 shrink-0 items-center justify-center [&_svg]:size-4\" style={noDrag}>{icon}</span> : null}\n      {title ? (\n        <div\n          data-slot=\"app-window-title\"\n          className={cn(\n            \"truncate font-semibold text-foreground/85 group-data-[state=inactive]/app-window:text-muted-foreground\",\n            win ? \"min-w-0 text-xs font-normal\" : \"pointer-events-none absolute inset-x-24 text-center text-sm\"\n          )}\n        >\n          {title}\n        </div>\n      ) : null}\n      {leading ? <div data-slot=\"app-window-leading\" className=\"relative z-10 flex items-center gap-1\" style={noDrag}>{leading}</div> : null}\n      {children ? <div data-slot=\"app-window-toolbar\" className=\"relative z-10 ms-auto flex min-w-0 items-center gap-1 text-foreground/70 group-data-[state=inactive]/app-window:text-foreground/40\" style={noDrag}>{children}</div> : null}\n      {controls && win ? <div className={cn(\"h-full\", children ? \"\" : \"ms-auto\")}><AppWindowControls /></div> : null}\n    </div>\n  )\n}\n\ntype AppWindowContentProps = React.ComponentProps<\"div\">\n\n/** The scrollable area under the title bar. */\nfunction AppWindowContent({ className, ...props }: AppWindowContentProps) {\n  return <div data-slot=\"app-window-content\" className={cn(\"min-h-0 flex-1 overflow-auto\", className)} {...props} />\n}\n\nexport { AppWindow, AppWindowTitleBar, AppWindowControls, AppWindowContent }\nexport type { AppWindowProps, AppWindowTitleBarProps, AppWindowControlsProps, AppWindowContentProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/app-window.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "window",
      "titlebar",
      "tauri",
      "electron",
      "desktop",
      "macos",
      "windows",
      "chrome",
      "drag region",
      "feel"
    ],
    "version": "1.0.1",
    "updated": "2026-10-11",
    "changelog": [
      {
        "version": "1.0.1",
        "date": "2026-10-11",
        "note": "macOS traffic lights are 12 px apart so every target meets the 24 px target-size rule (WCAG 2.5.8).",
        "breaking": false
      },
      {
        "version": "1.0.0",
        "date": "2026-10-12",
        "note": "First release.",
        "breaking": false
      }
    ],
    "tests": true,
    "ai": {
      "summary": "Real window chrome for a Tauri or Electron app. <AppWindow onClose onMinimize onMaximize><AppWindowTitleBar title=\"…\">toolbar</AppWindowTitleBar><AppWindowContent/></AppWindow>. It draws the platform's controls (macOS traffic lights on the left, Windows caption buttons on the right, none on iOS or web) from the page's feel, drags the window, and maximizes on double-click. You wire the callbacks; there is no Tauri or Electron dependency.",
      "whenToUse": [
        "The window of a Tauri or Electron app with a hidden native title bar (decorations: false, or titleBarStyle hidden)",
        "A desktop-style web app that should match the platform's window chrome"
      ],
      "whenNotToUse": [
        "Landing-page mockups of a Mac app (use mac-window)",
        "Modal dialogs inside a web app (use dialog)"
      ],
      "composesWith": [
        "feel-auto",
        "sidebar",
        "toolbar",
        "button"
      ],
      "a11y": [
        {
          "keys": "Tab",
          "action": "Reaches the window controls when their handlers are set, then the toolbar"
        },
        {
          "keys": "Enter / Space",
          "action": "Presses the focused window control"
        },
        {
          "keys": "Double-click the bar",
          "action": "Maximizes or restores (calls onMaximize)"
        }
      ],
      "customization": [
        "feel: 'macos' | 'windows' | 'ios' | 'web' overrides the page's feel for this window",
        "active: defaults to following real window focus; maximized switches the Windows button to restore and squares the corners",
        "onFullscreen: the macOS green button; hold Option to call onMaximize instead",
        "Tauri v2: onClose={() => getCurrentWindow().close()}; Electron: send an IPC message from the handler",
        "AppWindowControls renders only the controls, for a custom title bar"
      ]
    },
    "source": {
      "name": "tauri-controls",
      "url": "https://github.com/agmmnn/tauri-controls",
      "license": "MIT",
      "copyright": "Copyright (c) 2023 agmmnn",
      "modified": true
    },
    "examples": [
      "app-window-demo",
      "app-window-feels"
    ],
    "url": "https://ui.ballmac.com/components/app-window"
  },
  "categories": [
    "macos"
  ],
  "type": "registry:ui"
}