diff --git a/desktop/src/app/components/window/window.component.html b/desktop/src/app/components/window/window.component.html
new file mode 100644
index 0000000..411c04d
--- /dev/null
+++ b/desktop/src/app/components/window/window.component.html
@@ -0,0 +1,52 @@
+
+
+
+
+ {{ windowState.icon }}
+ {{ windowState.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/desktop/src/app/components/window/window.component.scss b/desktop/src/app/components/window/window.component.scss
new file mode 100644
index 0000000..c085ff2
--- /dev/null
+++ b/desktop/src/app/components/window/window.component.scss
@@ -0,0 +1,111 @@
+:host {
+ display: contents;
+}
+
+.butterfly-window {
+ display: flex;
+ flex-direction: column;
+ position: absolute;
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.25), 0 1px 4px rgba(0, 0, 0, 0.15);
+ transition: box-shadow 0.15s ease;
+
+ &.active {
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35), 0 2px 8px rgba(0, 0, 0, 0.2);
+ }
+
+ &.maximized {
+ border-radius: 0;
+ }
+}
+
+// ── Title bar ─────────────────────────────────────────────────────────────
+
+.window-titlebar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ height: 32px;
+ padding: 0 4px 0 12px;
+ background: linear-gradient(180deg, #3c3c3c 0%, #2d2d2d 100%);
+ color: #e0e0e0;
+ cursor: default;
+ flex-shrink: 0;
+ -webkit-app-region: drag;
+
+ .active & {
+ background: linear-gradient(180deg, #4a4a4a 0%, #3a3a3a 100%);
+ }
+}
+
+.titlebar-left {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+ overflow: hidden;
+}
+
+.window-icon {
+ font-size: 14px;
+ flex-shrink: 0;
+}
+
+.window-title {
+ font-size: 12px;
+ font-weight: 400;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ opacity: 0.95;
+}
+
+.titlebar-controls {
+ display: flex;
+ height: 100%;
+ -webkit-app-region: no-drag;
+}
+
+.titlebar-btn {
+ width: 46px;
+ height: 100%;
+ border: none;
+ background: transparent;
+ color: #e0e0e0;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background-color 0.1s;
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.1);
+ }
+
+ &.close-btn:hover {
+ background: #e81123;
+ color: #fff;
+ }
+}
+
+// ── Window body ───────────────────────────────────────────────────────────
+
+.window-body {
+ flex: 1;
+ background: #1e1e1e;
+ overflow: auto;
+ position: relative;
+}
+
+// ── Resize handle ─────────────────────────────────────────────────────────
+
+.window-resize-handle {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ width: 16px;
+ height: 16px;
+ cursor: nwse-resize;
+ z-index: 10;
+}
diff --git a/desktop/src/app/components/window/window.component.ts b/desktop/src/app/components/window/window.component.ts
new file mode 100644
index 0000000..5096c9e
--- /dev/null
+++ b/desktop/src/app/components/window/window.component.ts
@@ -0,0 +1,114 @@
+import {
+ Component,
+ Input,
+ Output,
+ EventEmitter,
+ ElementRef,
+ ViewChild,
+ AfterViewInit,
+ OnDestroy,
+ HostListener,
+} from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { type WindowState } from '../../services/window-manager.service';
+
+@Component({
+ selector: 'app-window',
+ standalone: true,
+ imports: [CommonModule],
+ templateUrl: './window.component.html',
+ styleUrl: './window.component.scss',
+})
+export class WindowComponent implements AfterViewInit, OnDestroy {
+ @Input({ required: true }) windowState!: WindowState;
+ @Input() appContent: unknown = null; // will be projected via ng-content
+
+ @Output() close = new EventEmitter();
+ @Output() minimize = new EventEmitter();
+ @Output() maximize = new EventEmitter();
+ @Output() focus = new EventEmitter();
+ @Output() geometryChange = new EventEmitter<{ id: string; x: number; y: number; width: number; height: number }>();
+
+ @ViewChild('titleBar') titleBar!: ElementRef;
+ @ViewChild('resizeHandle') resizeHandle!: ElementRef;
+
+ private isDragging = false;
+ private isResizing = false;
+ private dragOffsetX = 0;
+ private dragOffsetY = 0;
+ private resizeStartX = 0;
+ private resizeStartY = 0;
+ private resizeStartW = 0;
+ private resizeStartH = 0;
+
+ ngAfterViewInit(): void {
+ // Click on window body to focus.
+ this.element.addEventListener('mousedown', () => this.focus.emit(this.windowState.id));
+ }
+
+ ngOnDestroy(): void {
+ document.removeEventListener('mousemove', this.onMouseMove);
+ document.removeEventListener('mouseup', this.onMouseUp);
+ }
+
+ get element(): HTMLElement {
+ return this.elementRef.nativeElement;
+ }
+
+ constructor(private elementRef: ElementRef) {}
+
+ // ── Drag ───────────────────────────────────────────────────────────────
+
+ onTitleBarMouseDown(event: MouseEvent): void {
+ if (this.windowState.isMaximized) return;
+ event.preventDefault();
+ this.isDragging = true;
+ const rect = this.element.getBoundingClientRect();
+ this.dragOffsetX = event.clientX - rect.left;
+ this.dragOffsetY = event.clientY - rect.top;
+ document.addEventListener('mousemove', this.onMouseMove);
+ document.addEventListener('mouseup', this.onMouseUp);
+ this.focus.emit(this.windowState.id);
+ }
+
+ // ── Resize ─────────────────────────────────────────────────────────────
+
+ onResizeMouseDown(event: MouseEvent): void {
+ if (this.windowState.isMaximized) return;
+ event.preventDefault();
+ event.stopPropagation();
+ this.isResizing = true;
+ this.resizeStartX = event.clientX;
+ this.resizeStartY = event.clientY;
+ this.resizeStartW = this.windowState.width;
+ this.resizeStartH = this.windowState.height;
+ document.addEventListener('mousemove', this.onMouseMove);
+ document.addEventListener('mouseup', this.onMouseUp);
+ this.focus.emit(this.windowState.id);
+ }
+
+ private onMouseMove = (event: MouseEvent): void => {
+ if (this.isDragging) {
+ const x = event.clientX - this.dragOffsetX;
+ const y = event.clientY - this.dragOffsetY;
+ this.geometryChange.emit({ id: this.windowState.id, x, y, width: this.windowState.width, height: this.windowState.height });
+ } else if (this.isResizing) {
+ const dw = event.clientX - this.resizeStartX;
+ const dh = event.clientY - this.resizeStartY;
+ const width = Math.max(this.windowState.minWidth, this.resizeStartW + dw);
+ const height = Math.max(this.windowState.minHeight, this.resizeStartH + dh);
+ this.geometryChange.emit({ id: this.windowState.id, x: this.windowState.x, y: this.windowState.y, width, height });
+ }
+ };
+
+ private onMouseUp = (): void => {
+ this.isDragging = false;
+ this.isResizing = false;
+ document.removeEventListener('mousemove', this.onMouseMove);
+ document.removeEventListener('mouseup', this.onMouseUp);
+ };
+
+ onDoubleClickTitleBar(): void {
+ this.maximize.emit(this.windowState.id);
+ }
+}