diff --git a/web/src/components/MasonryView/MasonryColumn.tsx b/web/src/components/MasonryView/MasonryColumn.tsx
new file mode 100644
index 000000000..74b8d6ce1
--- /dev/null
+++ b/web/src/components/MasonryView/MasonryColumn.tsx
@@ -0,0 +1,43 @@
+import { MasonryItem } from "./MasonryItem";
+import { MasonryColumnProps } from "./types";
+
+/**
+ * Column component for masonry layout
+ *
+ * Responsibilities:
+ * - Render a single column in the masonry grid
+ * - Display prefix element in the first column (e.g., memo editor)
+ * - Render all assigned memo items in order
+ * - Pass render context to items (includes compact mode flag)
+ */
+export function MasonryColumn({
+  memoIndices,
+  memoList,
+  renderer,
+  renderContext,
+  onHeightChange,
+  isFirstColumn,
+  prefixElement,
+  prefixElementRef,
+}: MasonryColumnProps) {
+  return (
+    
+      {/* Prefix element (like memo editor) goes in first column */}
+      {isFirstColumn && prefixElement && 
{prefixElement}
}
+
+      {/* Render all memos assigned to this column */}
+      {memoIndices?.map((memoIndex) => {
+        const memo = memoList[memoIndex];
+        return memo ? (
+          
+        ) : null;
+      })}
+    
{renderer(memo, renderContext)}
;
+}
diff --git a/web/src/components/MasonryView/MasonryView.tsx b/web/src/components/MasonryView/MasonryView.tsx
index aca8369d5..249f8cb90 100644
--- a/web/src/components/MasonryView/MasonryView.tsx
+++ b/web/src/components/MasonryView/MasonryView.tsx
@@ -1,156 +1,42 @@
-import { useCallback, useEffect, useRef, useState } from "react";
+import { useMemo, useRef } from "react";
 import { cn } from "@/lib/utils";
-import { Memo } from "@/types/proto/api/v1/memo_service";
-
-interface Props {
-  memoList: Memo[];
-  renderer: (memo: Memo) => JSX.Element;
-  prefixElement?: JSX.Element;
-  listMode?: boolean;
-}
-
-interface MemoItemProps {
-  memo: Memo;
-  renderer: (memo: Memo) => JSX.Element;
-  onHeightChange: (memoName: string, height: number) => void;
-}
-
-// Minimum width required to show more than one column
-const MINIMUM_MEMO_VIEWPORT_WIDTH = 512;
-
-const MemoItem = ({ memo, renderer, onHeightChange }: MemoItemProps) => {
-  const itemRef = useRef(null);
-  const resizeObserverRef = useRef(null);
-
-  useEffect(() => {
-    if (!itemRef.current) return;
-
-    const measureHeight = () => {
-      if (itemRef.current) {
-        const height = itemRef.current.offsetHeight;
-        onHeightChange(memo.name, height);
-      }
-    };
-
-    measureHeight();
-
-    // Set up ResizeObserver to track dynamic content changes (images, expanded text, etc.)
-    resizeObserverRef.current = new ResizeObserver(measureHeight);
-    resizeObserverRef.current.observe(itemRef.current);
-
-    return () => {
-      resizeObserverRef.current?.disconnect();
-    };
-  }, [memo.name, onHeightChange]);
-
-  return {renderer(memo)}
;
-};
+import { MasonryColumn } from "./MasonryColumn";
+import { MasonryViewProps, MemoRenderContext } from "./types";
+import { useMasonryLayout } from "./useMasonryLayout";
 
 /**
- * Algorithm to distribute memos into columns based on height for balanced layout
- * Uses greedy approach: always place next memo in the shortest column
+ * Masonry layout component for displaying memos in a balanced, multi-column grid
+ *
+ * Features:
+ * - Responsive column count based on viewport width
+ * - Longest Processing-Time First (LPT) algorithm for optimal distribution
+ * - Pins editor and first memo to first column for stability
+ * - Debounced redistribution for performance
+ * - Automatic height tracking with ResizeObserver
+ * - Auto-enables compact mode in multi-column layouts
+ *
+ * The layout automatically adjusts to:
+ * - Window resizing
+ * - Content changes (images loading, text expansion)
+ * - Dynamic memo additions/removals
+ *
+ * Algorithm guarantee: Layout is never more than 34% longer than optimal (proven)
  */
-const distributeMemosToColumns = (
-  memos: Memo[],
-  columns: number,
-  itemHeights: Map,
-  prefixElementHeight: number = 0,
-): { distribution: number[][]; columnHeights: number[] } => {
-  // List mode: all memos in single column
-  if (columns === 1) {
-    const totalHeight = memos.reduce((sum, memo) => sum + (itemHeights.get(memo.name) || 0), prefixElementHeight);
-    return {
-      distribution: [Array.from({ length: memos.length }, (_, i) => i)],
-      columnHeights: [totalHeight],
-    };
-  }
-
-  // Initialize columns and heights
-  const distribution: number[][] = Array.from({ length: columns }, () => []);
-  const columnHeights: number[] = Array(columns).fill(0);
-
-  // Add prefix element height to first column
-  if (prefixElementHeight > 0) {
-    columnHeights[0] = prefixElementHeight;
-  }
-
-  // Distribute each memo to the shortest column
-  memos.forEach((memo, index) => {
-    const height = itemHeights.get(memo.name) || 0;
-
-    // Find column with minimum height
-    const shortestColumnIndex = columnHeights.indexOf(Math.min(...columnHeights));
-
-    distribution[shortestColumnIndex].push(index);
-    columnHeights[shortestColumnIndex] += height;
-  });
-
-  return { distribution, columnHeights };
-};
-
-const MasonryView = (props: Props) => {
-  const [columns, setColumns] = useState(1);
-  const [itemHeights, setItemHeights] = useState