[web] Add browser scroll controller and platform channel integration
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine.dart b/engine/src/flutter/lib/web_ui/lib/src/engine.dart
index d56b9ec..d608f09 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine.dart
@@ -19,6 +19,7 @@
 export 'engine/app_bootstrap.dart';
 export 'engine/arena.dart';
 export 'engine/browser_detection.dart';
+export 'engine/browser_scroll_controller.dart';
 export 'engine/canvaskit/canvas.dart';
 export 'engine/canvaskit/canvaskit_api.dart';
 export 'engine/canvaskit/color_filter.dart';
@@ -83,6 +84,7 @@
 export 'engine/noto_font_encoding.dart';
 export 'engine/occlusion_map.dart';
 export 'engine/onscreen_logging.dart';
+export 'engine/platform_channels/browser_scroll_channel.dart';
 export 'engine/platform_dispatcher.dart';
 export 'engine/platform_dispatcher/app_lifecycle_state.dart';
 export 'engine/platform_dispatcher/view_focus_binding.dart';
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/browser_scroll_controller.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/browser_scroll_controller.dart
new file mode 100644
index 0000000..ca088bd
--- /dev/null
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/browser_scroll_controller.dart
@@ -0,0 +1,341 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:typed_data';
+
+import 'package:ui/src/engine.dart';
+import 'package:ui/ui.dart' as ui;
+
+/// Enables browser-native scrolling for a Flutter view.
+///
+/// When enabled:
+/// - The Flutter view becomes position:fixed
+/// - A placeholder element defines the scrollable extent
+/// - Browser handles all scroll events naturally
+/// - Flutter compensates rendering based on scroll position
+///
+/// This solves the nested scrolling problem where iframes or HTML content
+/// would otherwise block Flutter's scroll detection.
+class BrowserScrollController {
+  BrowserScrollController(this._view);
+
+  final EngineFlutterView _view;
+  DomElement? _placeholder;
+  DomEventListener? _scrollListener;
+  bool _enabled = false;
+  double _currentScrollY = 0.0;
+
+  /// Whether browser-driven scrolling is currently enabled.
+  bool get isEnabled => _enabled;
+
+  /// Current browser scroll position.
+  double get scrollY => _currentScrollY;
+
+  /// Enable browser-driven scrolling mode.
+  void enable() {
+    print('[DEBUG BrowserScrollController] enable() called');
+
+    if (_enabled) {
+      print('[DEBUG BrowserScrollController] Already enabled, returning');
+      return;
+    }
+
+    print('[DEBUG BrowserScrollController] Starting enable process...');
+    final DomElement rootElement = _view.dom.rootElement;
+
+    assert(() {
+      print('[DEBUG] rootElement tag: ${rootElement.tagName}');
+      print('[DEBUG] rootElement id: ${rootElement.id}');
+      print('[DEBUG] rootElement parent: ${rootElement.parent?.tagName}');
+      print('[DEBUG] rootElement current position: ${rootElement.style.position}');
+      return true;
+    }());
+
+    // Create placeholder that will define scroll extent
+    // Create a simple div instead of cloning to avoid inherited styles
+    _placeholder = domDocument.createElement('div');
+    _placeholder!.id = 'flt-browser-scroll-placeholder';
+    _placeholder!.style
+      ..position =
+          'relative' // Must be in document flow to create scrollable space
+      ..display =
+          'block' // Block element respects height
+      ..height = '0px'
+      ..width = '100%'
+      ..pointerEvents =
+          'none' // Don't intercept pointer events
+      ..visibility =
+          'hidden' // Invisible but takes up space
+      ..margin = '0'
+      ..padding = '0';
+
+    assert(() {
+      print('[DEBUG] Placeholder created with id: ${_placeholder!.id}');
+      return true;
+    }());
+
+    // Insert placeholder before Flutter view in DOM
+    rootElement.parent!.insertBefore(_placeholder!, rootElement);
+
+    assert(() {
+      print('[DEBUG] Placeholder inserted into DOM');
+      return true;
+    }());
+
+    // Make Flutter view fixed to viewport
+    // Remove 'inset' and 'bottom' to allow explicit height control
+    rootElement.style.removeProperty('inset');
+    rootElement.style.removeProperty('bottom');
+
+    rootElement.style
+      ..position = 'fixed'
+      ..top = '0'
+      ..left = '0'
+      ..right = '0'
+      ..overflow = 'hidden'; // Flutter handles internal scrolling
+    // Height will be set by updateScrollExtent
+
+    assert(() {
+      print('[DEBUG] Flutter view style updated to fixed');
+      print('[DEBUG] Position: ${rootElement.style.position}');
+      print('[DEBUG] Top: ${rootElement.style.top}');
+      print('[DEBUG] Left: ${rootElement.style.left}');
+      print('[DEBUG] Right: ${rootElement.style.right}');
+      print('[DEBUG] Bottom: ${rootElement.style.bottom}');
+      print('[DEBUG] Height: ${rootElement.style.height}');
+      print('[DEBUG] Inset: ${rootElement.style.getPropertyValue("inset")}');
+      return true;
+    }());
+
+    // Enable scrolling on body to allow browser-driven scrolling
+    // Inject CSS to override Flutter's default fixed positioning
+    final DomHTMLStyleElement styleElement =
+        domDocument.createElement('style') as DomHTMLStyleElement;
+    styleElement.id = 'flt-browser-scroll-style';
+    styleElement.text = '''
+      html, body {
+        height: auto !important;
+        min-height: 100% !important;
+        position: static !important;
+        overflow: auto !important;
+      }
+    ''';
+    domDocument.head!.append(styleElement);
+
+    assert(() {
+      print('[DEBUG] Browser scroll CSS injected');
+      return true;
+    }());
+
+    // Listen to scroll events on window
+    _scrollListener = createDomEventListener(_onScroll);
+    domWindow.addEventListener('scroll', _scrollListener);
+
+    // Also listen for resize events to maintain the height
+    _resizeListener = createDomEventListener((DomEvent event) {
+      assert(() {
+        print('[DEBUG] Window resize detected, re-enforcing height');
+        return true;
+      }());
+      _enforceHeight();
+    });
+    domWindow.addEventListener('resize', _resizeListener);
+
+    // Mark as enabled
+    _enabled = true;
+
+    assert(() {
+      print('[BrowserScrollController] Enabled for view ${_view.viewId}');
+      print('[BrowserScrollController] Waiting for framework to send content height...');
+      return true;
+    }());
+  }
+
+  void _onScroll(DomEvent event) {
+    if (!_enabled || _placeholder == null) {
+      return;
+    }
+
+    // Get current scroll position
+    final double scrollY =
+        domDocument.documentElement?.scrollTop ?? domDocument.body?.scrollTop ?? 0.0;
+
+    // Move the Flutter view up to compensate for scroll
+    // This creates the illusion that the Flutter content is scrolling
+    _view.dom.rootElement.style.transform = 'translateY(-${scrollY}px)';
+
+    // Re-enforce height on every scroll to fight Flutter's resize logic
+    _enforceHeight();
+
+    _currentScrollY = scrollY;
+
+    // Send scroll position to framework
+    _sendScrollPositionToFramework(_currentScrollY);
+  }
+
+  /// Update the scrollable extent (height).
+  ///
+  /// Called when Flutter's content height changes to update the browser's
+  /// scrollable area.
+  void updateScrollExtent(double height) {
+    print('[DEBUG] updateScrollExtent called with height: $height');
+    print('[DEBUG] _placeholder: ${_placeholder != null ? "exists" : "null"}');
+    print('[DEBUG] _enabled: $_enabled');
+
+    if (_placeholder != null && _enabled) {
+      _placeholder!.style.height = '${height}px';
+
+      // Store the desired height for continuous enforcement
+      _desiredHeight = height;
+
+      // Set Flutter view's height to match content so it renders everything
+      _enforceHeight();
+
+      assert(() {
+        print('[BrowserScrollController] Updated scroll extent to ${height}px');
+        print('[DEBUG] Placeholder actual height style: ${_placeholder!.style.height}');
+        print('[DEBUG] Flutter view height: ${_view.dom.rootElement.style.height}');
+        return true;
+      }());
+    } else {
+      print('[DEBUG] Skipping update - placeholder null or not enabled');
+    }
+  }
+
+  double? _desiredHeight;
+  DomEventListener? _resizeListener;
+  DomMutationObserver? _heightObserver;
+
+  /// Enforce the height setting, preventing Flutter's resize logic from overriding it.
+  void _enforceHeight() {
+    if (_desiredHeight == null) return;
+
+    final DomElement rootElement = _view.dom.rootElement;
+
+    // Use setProperty with priority 'important' to prevent Flutter's resize logic from overriding
+    rootElement.style.setProperty('height', '${_desiredHeight}px', 'important');
+
+    // Also force min-height to prevent collapsing
+    rootElement.style.setProperty('min-height', '${_desiredHeight}px', 'important');
+
+    // Set up a MutationObserver to aggressively prevent any height changes
+    if (_heightObserver == null) {
+      _heightObserver = createDomMutationObserver((
+        JSArray<JSAny?> mutations,
+        DomMutationObserver observer,
+      ) {
+        // Check if height was changed by Flutter's resize logic
+        final String? currentHeight = rootElement.style.height;
+        if (currentHeight != null && !currentHeight.contains('${_desiredHeight}px')) {
+          // Flutter tried to change the height - revert it immediately!
+          assert(() {
+            print(
+              '[DEBUG] 🛡️ Height override detected ($currentHeight), reverting to ${_desiredHeight}px',
+            );
+            return true;
+          }());
+
+          // Temporarily disconnect to avoid triggering ourselves
+          observer.disconnect();
+
+          rootElement.style.setProperty('height', '${_desiredHeight}px', 'important');
+          rootElement.style.setProperty('min-height', '${_desiredHeight}px', 'important');
+
+          // Reconnect to continue monitoring
+          observer.observe(rootElement, attributes: true, attributeFilter: <String>['style']);
+        }
+      });
+
+      // Observe style attribute changes
+      _heightObserver!.observe(rootElement, attributes: true, attributeFilter: <String>['style']);
+
+      assert(() {
+        print('[DEBUG] 🛡️ MutationObserver installed to protect height');
+        return true;
+      }());
+    }
+
+    assert(() {
+      print('[DEBUG] _enforceHeight() set height to ${_desiredHeight}px');
+      return true;
+    }());
+  }
+
+  void _sendScrollPositionToFramework(double scrollY) {
+    // Send scroll position update as a method call to the framework
+    final ByteData? message = const StandardMethodCodec().encodeMethodCall(
+      MethodCall('updateScrollPosition', <String, dynamic>{
+        'viewId': _view.viewId,
+        'scrollY': scrollY,
+      }),
+    );
+
+    EnginePlatformDispatcher.instance.invokeOnPlatformMessage(
+      'flutter/browserscroll',
+      message,
+      (ByteData? _) {}, // Empty callback - no response needed
+    );
+  }
+
+  /// Disable browser-driven scrolling and restore normal Flutter mode.
+  /// Automatically sets a large default scroll extent for browser scrolling
+  /// This provides a reasonable default that works for most content
+
+  void disable() {
+    if (!_enabled) {
+      return;
+    }
+
+    // Restore normal positioning
+    _view.dom.rootElement.style
+      ..removeProperty('position')
+      ..removeProperty('top')
+      ..removeProperty('left')
+      ..removeProperty('right')
+      ..removeProperty('bottom')
+      ..removeProperty('height')
+      ..removeProperty('min-height')
+      ..removeProperty('overflow')
+      ..removeProperty('transform');
+
+    // Remove injected CSS
+    domDocument.getElementById('flt-browser-scroll-style')?.remove();
+
+    // Remove placeholder
+    _placeholder?.remove();
+    _placeholder = null;
+
+    // Remove scroll listener
+    if (_scrollListener != null) {
+      domWindow.removeEventListener('scroll', _scrollListener);
+      _scrollListener = null;
+    }
+
+    // Remove resize listener
+    if (_resizeListener != null) {
+      domWindow.removeEventListener('resize', _resizeListener);
+      _resizeListener = null;
+    }
+
+    // Disconnect MutationObserver
+    if (_heightObserver != null) {
+      _heightObserver!.disconnect();
+      _heightObserver = null;
+    }
+
+    _enabled = false;
+    _currentScrollY = 0.0;
+    _desiredHeight = null;
+
+    assert(() {
+      print('[BrowserScrollController] Disabled for view ${_view.viewId}');
+      return true;
+    }());
+  }
+
+  /// Clean up resources.
+  void dispose() {
+    disable();
+  }
+}
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_channels/browser_scroll_channel.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_channels/browser_scroll_channel.dart
new file mode 100644
index 0000000..eae2362
--- /dev/null
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_channels/browser_scroll_channel.dart
@@ -0,0 +1,182 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:typed_data';
+
+import 'package:ui/src/engine.dart';
+import 'package:ui/ui.dart' as ui;
+
+/// Platform channel for browser scroll coordination between engine and framework.
+///
+/// This channel allows the framework to:
+/// - Enable/disable browser scrolling mode
+/// - Update scroll extent when content changes
+/// - Receive scroll position updates from the browser
+class BrowserScrollChannel {
+  /// The channel name used for browser scroll communication.
+  static const String channelName = 'flutter/browserscroll';
+
+  static const MethodCodec _codec = StandardMethodCodec();
+
+  /// Handle incoming messages from the framework.
+  static void handleMessage(ByteData? message, ui.PlatformMessageResponseCallback? callback) {
+    print('[DEBUG BrowserScrollChannel] handleMessage called');
+
+    if (message == null) {
+      print('[DEBUG BrowserScrollChannel] Message is null!');
+      _sendErrorResponse(callback, 'invalid_message', 'Message is null', null);
+      return;
+    }
+
+    try {
+      print('[DEBUG BrowserScrollChannel] Decoding method call...');
+      final MethodCall methodCall = _codec.decodeMethodCall(message);
+      print('[DEBUG BrowserScrollChannel] Method: ${methodCall.method}');
+      print('[DEBUG BrowserScrollChannel] Arguments: ${methodCall.arguments}');
+
+      // Convert arguments to Map<String, dynamic>
+      final Map<String, dynamic> args;
+      if (methodCall.arguments == null) {
+        args = <String, dynamic>{};
+      } else if (methodCall.arguments is Map) {
+        args = Map<String, dynamic>.from(methodCall.arguments as Map);
+      } else {
+        _sendErrorResponse(
+          callback,
+          'invalid_args',
+          'Arguments must be a Map, got: ${methodCall.arguments.runtimeType}',
+          null,
+        );
+        return;
+      }
+
+      switch (methodCall.method) {
+        case 'enable':
+          _handleEnable(args, callback);
+          break;
+
+        case 'disable':
+          _handleDisable(args, callback);
+          break;
+
+        case 'updateExtent':
+          _handleUpdateExtent(args, callback);
+          break;
+
+        default:
+          _sendErrorResponse(
+            callback,
+            'unknown_method',
+            'Unknown method: ${methodCall.method}',
+            null,
+          );
+      }
+    } catch (e, stackTrace) {
+      print('[DEBUG BrowserScrollChannel] Error: $e');
+      print('[DEBUG BrowserScrollChannel] Stack: $stackTrace');
+      _sendErrorResponse(callback, 'error', 'Error handling message: $e', null);
+    }
+  }
+
+  static void _handleEnable(
+    Map<String, dynamic> args,
+    ui.PlatformMessageResponseCallback? callback,
+  ) {
+    print('[DEBUG BrowserScrollChannel] _handleEnable called with args: $args');
+
+    final int? viewId = args['viewId'] as int?;
+
+    if (viewId == null) {
+      print('[DEBUG BrowserScrollChannel] viewId is null!');
+      _sendErrorResponse(callback, 'invalid_args', 'viewId is required', null);
+      return;
+    }
+
+    print('[DEBUG BrowserScrollChannel] Looking for view $viewId...');
+    final EngineFlutterView? view = EnginePlatformDispatcher.instance.viewManager[viewId];
+
+    if (view == null) {
+      print('[DEBUG BrowserScrollChannel] View $viewId not found!');
+      _sendErrorResponse(callback, 'view_not_found', 'View not found: $viewId', null);
+      return;
+    }
+
+    print('[DEBUG BrowserScrollChannel] Calling view.enableBrowserScrolling()...');
+    view.enableBrowserScrolling();
+    print('[DEBUG BrowserScrollChannel] Browser scrolling enabled successfully!');
+    _sendSuccessResponse(callback, true);
+  }
+
+  static void _handleDisable(
+    Map<String, dynamic> args,
+    ui.PlatformMessageResponseCallback? callback,
+  ) {
+    final int? viewId = args['viewId'] as int?;
+
+    if (viewId == null) {
+      _sendErrorResponse(callback, 'invalid_args', 'viewId is required', null);
+      return;
+    }
+
+    final EngineFlutterView? view = EnginePlatformDispatcher.instance.viewManager[viewId];
+
+    if (view == null) {
+      _sendErrorResponse(callback, 'view_not_found', 'View not found: $viewId', null);
+      return;
+    }
+
+    view.disableBrowserScrolling();
+    _sendSuccessResponse(callback, true);
+  }
+
+  static void _handleUpdateExtent(
+    Map<String, dynamic> args,
+    ui.PlatformMessageResponseCallback? callback,
+  ) {
+    final int? viewId = args['viewId'] as int?;
+    final double? height = (args['height'] as num?)?.toDouble();
+
+    if (viewId == null || height == null) {
+      _sendErrorResponse(callback, 'invalid_args', 'viewId and height are required', null);
+      return;
+    }
+
+    final EngineFlutterView? view = EnginePlatformDispatcher.instance.viewManager[viewId];
+
+    if (view == null) {
+      _sendErrorResponse(callback, 'view_not_found', 'View not found: $viewId', null);
+      return;
+    }
+
+    view.updateBrowserScrollExtent(height);
+    _sendSuccessResponse(callback, true);
+  }
+
+  static void _sendSuccessResponse(ui.PlatformMessageResponseCallback? callback, dynamic result) {
+    if (callback == null) {
+      return;
+    }
+
+    final ByteData? responseData = _codec.encodeSuccessEnvelope(result);
+    callback(responseData);
+  }
+
+  static void _sendErrorResponse(
+    ui.PlatformMessageResponseCallback? callback,
+    String code,
+    String? message,
+    dynamic details,
+  ) {
+    if (callback == null) {
+      return;
+    }
+
+    final ByteData? responseData = _codec.encodeErrorEnvelope(
+      code: code,
+      message: message,
+      details: details,
+    );
+    callback(responseData);
+  }
+}
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart
index 09c1ec4..e1a4f20 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart
@@ -609,6 +609,10 @@
         PlatformViewMessageHandler.instance.handlePlatformViewCall(method, arguments, callback!);
         return;
 
+      case BrowserScrollChannel.channelName:
+        BrowserScrollChannel.handleMessage(data, callback);
+        return;
+
       case 'flutter/accessibility':
         // In widget tests we want to bypass processing of platform messages.
         const StandardMessageCodec codec = StandardMessageCodec();
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/content_manager.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/content_manager.dart
index d27c70c..4b5118b 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/content_manager.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/content_manager.dart
@@ -64,6 +64,9 @@
     return _contents.containsKey(viewId);
   }
 
+  /// Track if we've shown the browser scroll hint
+  bool _hasShownBrowserScrollHint = false;
+
   /// Returns the cached contents of [viewId], to be injected into the DOM.
   ///
   /// This is only used by the active `Renderer` object when a platform view needs
@@ -166,10 +169,55 @@
 
       wrapper.setAttribute(_ariaHiddenAttribute, 'true');
 
+      // Auto-detection: Suggest browser scrolling when first platform view is created
+      _maybeSuggestBrowserScrolling();
+
       return wrapper;
     });
   }
 
+  /// Suggest enabling browser scrolling when platform views are detected.
+  ///
+  /// This helps users discover the solution to nested scrolling problems
+  /// with iframes and HTML content.
+  void _maybeSuggestBrowserScrolling() {
+    if (_hasShownBrowserScrollHint || _contents.length != 1) {
+      return;
+    }
+
+    _hasShownBrowserScrollHint = true;
+
+    assert(() {
+      print(
+        '\n'
+        '╔═══════════════════════════════════════════════════════════════════════════╗\n'
+        '║  Platform View Detected - Browser Scrolling Available                    ║\n'
+        '╠═══════════════════════════════════════════════════════════════════════════╣\n'
+        '║                                                                           ║\n'
+        '║  If you experience scrolling issues with iframes or HTML content,        ║\n'
+        '║  consider enabling browser-driven scrolling mode:                        ║\n'
+        '║                                                                           ║\n'
+        '║  // In your Flutter app (requires framework support):                    ║\n'
+        '║  BrowserScrollView(                                                      ║\n'
+        '║    child: YourContent(),                                                 ║\n'
+        '║  )                                                                        ║\n'
+        '║                                                                           ║\n'
+        '║  // Or enable via JavaScript console for testing:                        ║\n'
+        '║  const channel = new MessageChannel();                                   ║\n'
+        '║  window.flutter.postMessage({                                            ║\n'
+        '║    method: "enable",                                                     ║\n'
+        '║    viewId: 0                                                             ║\n'
+        '║  }, "flutter/browserscroll", [channel.port2]);                           ║\n'
+        '║                                                                           ║\n'
+        '║  This solves the nested scrolling problem where scrolling stops          ║\n'
+        '║  working when the pointer is over platform views (iframes, HTML).        ║\n'
+        '║                                                                           ║\n'
+        '╚═══════════════════════════════════════════════════════════════════════════╝\n',
+      );
+      return true;
+    }());
+  }
+
   /// Removes a PlatformView by its `viewId` from the manager, and from the DOM.
   ///
   /// Once a view has been cleared, calls to [knowsViewId] will fail, as if it had
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/pointer_binding.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/pointer_binding.dart
index ae33a76..269a896 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/pointer_binding.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/pointer_binding.dart
@@ -764,6 +764,20 @@
   }
 
   void _handleWheelEvent(DomEvent event) {
+    // DEBUG: Log wheel event handling
+    print('[DEBUG] _handleWheelEvent called, browserScrollEnabled=${_view.isBrowserScrollEnabled}');
+
+    // If browser scroll mode is enabled, let the browser handle wheel events
+    // naturally instead of Flutter intercepting them. This solves the nested
+    // scrolling problem with iframes and HTML content.
+    if (_view.isBrowserScrollEnabled) {
+      // Don't call preventDefault() - let browser scroll
+      print('[DEBUG] Browser scroll enabled - NOT calling preventDefault()');
+      return;
+    }
+
+    print('[DEBUG] Browser scroll NOT enabled - calling preventDefault()');
+
     // Wheel events should switch semantics to pointer event mode, because wheel
     // events should always be handled by the framework.
     // See: https://github.com/flutter/flutter/issues/159358
@@ -931,11 +945,11 @@
   double? _currentY;
   num? _startTime;
   bool _isScrollGesture = false;
-  
+
   // Thresholds for detecting scroll gestures
   static const double _scrollThreshold = 10.0; // pixels
   static const double _scrollAngleThreshold = 0.5; // radians (~30 degrees)
-  
+
   void onPointerDown(DomPointerEvent event) {
     _startX = event.clientX.toDouble();
     _startY = event.clientY.toDouble();
@@ -943,39 +957,43 @@
     _currentY = _startY;
     _startTime = event.timeStamp;
     _isScrollGesture = false;
-    
+
     if (_debugLogPointerEvents) {
       print('[TOUCH_GESTURE] Down at ($_startX, $_startY)');
     }
   }
-  
+
   void onPointerMove(DomPointerEvent event) {
     _currentX = event.clientX.toDouble();
     _currentY = event.clientY.toDouble();
-    
+
     if (_startX != null && _startY != null) {
       final double deltaX = (_currentX! - _startX!).abs();
       final double deltaY = (_currentY! - _startY!).abs();
-      
+
       // Check if movement exceeds threshold
       if (deltaY > _scrollThreshold || deltaX > _scrollThreshold) {
         // Check if it's primarily vertical movement
         if (deltaY > deltaX * 1.5) {
           _isScrollGesture = true;
           if (_debugLogPointerEvents) {
-            print('[TOUCH_GESTURE] Detected vertical scroll gesture: deltaX=$deltaX, deltaY=$deltaY');
+            print(
+              '[TOUCH_GESTURE] Detected vertical scroll gesture: deltaX=$deltaX, deltaY=$deltaY',
+            );
           }
         } else if (deltaX > deltaY * 1.5) {
           // Horizontal scroll - also consider it a scroll gesture
           _isScrollGesture = true;
           if (_debugLogPointerEvents) {
-            print('[TOUCH_GESTURE] Detected horizontal scroll gesture: deltaX=$deltaX, deltaY=$deltaY');
+            print(
+              '[TOUCH_GESTURE] Detected horizontal scroll gesture: deltaX=$deltaX, deltaY=$deltaY',
+            );
           }
         }
       }
     }
   }
-  
+
   void reset() {
     _startX = null;
     _startY = null;
@@ -984,9 +1002,9 @@
     _startTime = null;
     _isScrollGesture = false;
   }
-  
+
   bool get isScrollGesture => _isScrollGesture;
-  
+
   bool get hasMovedSignificantly {
     if (_startX == null || _startY == null || _currentX == null || _currentY == null) {
       return false;
@@ -1080,9 +1098,11 @@
           () => _TouchGestureTracker(),
         );
         tracker.onPointerDown(event);
-        
+
         if (_debugLogPointerEvents) {
-          print('[TOUCH_PASSTHROUGH] Touch pointerdown - NOT calling preventDefault() to allow browser scroll');
+          print(
+            '[TOUCH_PASSTHROUGH] Touch pointerdown - NOT calling preventDefault() to allow browser scroll',
+          );
         }
       }
 
@@ -1107,7 +1127,9 @@
             );
           });
         } else if (_debugLogPointerEvents) {
-          print('[TOUCH_PASSTHROUGH] Skipping preventDefault() for touch event to enable browser scroll');
+          print(
+            '[TOUCH_PASSTHROUGH] Skipping preventDefault() for touch event to enable browser scroll',
+          );
         }
       }
     });
@@ -1130,7 +1152,7 @@
       final _ButtonSanitizer sanitizer = _ensureSanitizer(device);
       final List<ui.PointerData> pointerData = <ui.PointerData>[];
       final List<DomPointerEvent> expandedEvents = _expandEvents(moveEvent);
-      
+
       // Track touch gesture movement
       if (moveEvent.pointerType == 'touch') {
         final _TouchGestureTracker? tracker = _touchTrackers[device];
@@ -1141,7 +1163,7 @@
           }
         }
       }
-      
+
       for (final DomPointerEvent event in expandedEvents) {
         final _SanitizedDetails? up = sanitizer.sanitizeMissingRightClickUp(
           buttons: event.buttons!.toInt(),
@@ -1194,7 +1216,7 @@
           _callback(event, pointerData);
         }
       }
-      
+
       // Clean up touch tracker
       if (event.pointerType == 'touch') {
         _touchTrackers.remove(device);
@@ -1217,7 +1239,7 @@
         _convertEventsToPointerData(data: pointerData, event: event, details: details);
         _callback(event, pointerData);
       }
-      
+
       // Clean up touch tracker
       if (event.pointerType == 'touch') {
         _touchTrackers.remove(device);
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/window.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/window.dart
index 40a4935..717933b 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/window.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/window.dart
@@ -110,6 +110,7 @@
     _resizeSubscription.cancel();
     dimensionsProvider.close();
     pointerBinding.dispose();
+    _browserScrollController?.dispose();
     dom.rootElement.remove();
     // TODO(harryterkelsen): What should we do about this in multi-view?
     renderer.clearFragmentProgramCache();
@@ -165,6 +166,34 @@
 
   late final EngineSemanticsOwner semantics = EngineSemanticsOwner(viewId, dom.semanticsHost);
 
+  /// Browser scroll controller (null if browser scrolling not enabled).
+  BrowserScrollController? _browserScrollController;
+
+  /// Whether browser-driven scrolling is enabled for this view.
+  bool get isBrowserScrollEnabled => _browserScrollController?.isEnabled ?? false;
+
+  /// Enable browser-driven scrolling for this view.
+  ///
+  /// This makes the browser handle all scrolling instead of Flutter,
+  /// which solves the nested scrolling problem with iframes and HTML content.
+  void enableBrowserScrolling() {
+    _browserScrollController ??= BrowserScrollController(this);
+    _browserScrollController!.enable();
+  }
+
+  /// Disable browser-driven scrolling and restore normal Flutter mode.
+  void disableBrowserScrolling() {
+    _browserScrollController?.disable();
+  }
+
+  /// Update the browser scroll extent (total content height).
+  ///
+  /// Called when Flutter's content height changes to update the browser's
+  /// scrollable area.
+  void updateBrowserScrollExtent(double height) {
+    _browserScrollController?.updateScrollExtent(height);
+  }
+
   @override
   ui.Size get physicalSize {
     return _physicalSize ??= _computePhysicalSize();