/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @format
 * @flow strict-local
 */
/**
 * MIT License
 *
 * Copyright (C) 2026 Huawei Device Co., Ltd.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

//RNOH: patch - fix imports
import type { HostComponent } from "react-native/Libraries/Renderer/shims/ReactNativeTypes";
import type { EdgeInsetsProp } from "react-native/Libraries/StyleSheet/EdgeInsetsPropType";
import type { PointProp } from "react-native/Libraries/StyleSheet/PointPropType";
import type { ViewStyleProp } from "react-native/Libraries/StyleSheet/StyleSheet";
import type { ColorValue } from "react-native/Libraries/StyleSheet/StyleSheet";
import type {
  LayoutEvent,
  PressEvent,
  ScrollEvent,
} from "react-native/Libraries/Types/CoreEventTypes";
import type { EventSubscription } from "react-native/Libraries/vendor/emitter/EventEmitter";
import type {
  KeyboardEvent,
  KeyboardMetrics,
} from "react-native/Libraries/Components/Keyboard/Keyboard";
import type { ViewProps } from "react-native/Libraries/Components/View/ViewPropTypes";
import type { Props as ScrollViewStickyHeaderProps } from "react-native/Libraries/Components/ScrollView/ScrollViewStickyHeader";

import AnimatedImplementation from "react-native/Libraries/Animated/AnimatedImplementation";
import FrameRateLogger from "react-native/Libraries/Interaction/FrameRateLogger";
import { findNodeHandle } from "react-native/Libraries/ReactNative/RendererProxy";
import UIManager from "react-native/Libraries/ReactNative/UIManager";
import flattenStyle from "react-native/Libraries/StyleSheet/flattenStyle";
import splitLayoutProps from "react-native/Libraries/StyleSheet/splitLayoutProps";
import StyleSheet from "react-native/Libraries/StyleSheet/StyleSheet";
import Dimensions from "react-native/Libraries/Utilities/Dimensions";
import dismissKeyboard from "react-native/Libraries/Utilities/dismissKeyboard";
import Platform from "@react-native-oh/react-native-harmony/Libraries/Utilities/Platform";
import Keyboard from "react-native/Libraries/Components/Keyboard/Keyboard";
import TextInputState from "@react-native-oh/react-native-harmony/Libraries/Components/TextInput/TextInputState";
import View from "react-native/Libraries/Components/View/View";
import AndroidHorizontalScrollContentViewNativeComponent from "react-native/Libraries/Components/ScrollView/AndroidHorizontalScrollContentViewNativeComponent";
import AndroidHorizontalScrollViewNativeComponent from "react-native/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent";
import processDecelerationRate from "@react-native-oh/react-native-harmony/Libraries/Components/ScrollView/processDecelerationRate.harmony";
import ScrollContentViewNativeComponent from "react-native/Libraries/Components/ScrollView/ScrollContentViewNativeComponent";
import Commands from "react-native/Libraries/Components/ScrollView/ScrollViewCommands";
import ScrollViewContext, {
  HORIZONTAL,
  VERTICAL,
} from "react-native/Libraries/Components/ScrollView/ScrollViewContext";
import ScrollViewNativeComponent from "react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent";
import ScrollViewStickyHeader from "react-native/Libraries/Components/ScrollView/ScrollViewStickyHeader";
import invariant from "invariant";
import memoize from "memoize-one";
import nullthrows from "nullthrows";
import * as React from "react";

if (Platform.OS === "ios") {
  require("react-native/Libraries/Renderer/shims/ReactNative"); // Force side effects to prevent T55744311
}

const { NativeHorizontalScrollViewTuple, NativeVerticalScrollViewTuple } =
  Platform.OS === "android"
    ? {
        NativeHorizontalScrollViewTuple: [
          AndroidHorizontalScrollViewNativeComponent,
          AndroidHorizontalScrollContentViewNativeComponent,
        ],
        NativeVerticalScrollViewTuple: [ScrollViewNativeComponent, View],
      }
    : {
        NativeHorizontalScrollViewTuple: [
          ScrollViewNativeComponent,
          ScrollContentViewNativeComponent,
        ],
        NativeVerticalScrollViewTuple: [
          ScrollViewNativeComponent,
          ScrollContentViewNativeComponent,
        ],
      };

/*
 * iOS scroll event timing nuances:
 * ===============================
 *
 *
 * Scrolling without bouncing, if you touch down:
 * -------------------------------
 *
 * 1. `onMomentumScrollBegin` (when animation begins after letting up)
 *    ... physical touch starts ...
 * 2. `onTouchStartCapture`   (when you press down to stop the scroll)
 * 3. `onTouchStart`          (same, but bubble phase)
 * 4. `onResponderRelease`    (when lifting up - you could pause forever before * lifting)
 * 5. `onMomentumScrollEnd`
 *
 *
 * Scrolling with bouncing, if you touch down:
 * -------------------------------
 *
 * 1. `onMomentumScrollBegin` (when animation begins after letting up)
 *    ... bounce begins ...
 *    ... some time elapses ...
 *    ... physical touch during bounce ...
 * 2. `onMomentumScrollEnd`   (Makes no sense why this occurs first during bounce)
 * 3. `onTouchStartCapture`   (immediately after `onMomentumScrollEnd`)
 * 4. `onTouchStart`          (same, but bubble phase)
 * 5. `onTouchEnd`            (You could hold the touch start for a long time)
 * 6. `onMomentumScrollBegin` (When releasing the view starts bouncing back)
 *
 * So when we receive an `onTouchStart`, how can we tell if we are touching
 * *during* an animation (which then causes the animation to stop)? The only way
 * to tell is if the `touchStart` occurred immediately after the
 * `onMomentumScrollEnd`.
 *
 * This is abstracted out for you, so you can just call this.scrollResponderIsAnimating() if
 * necessary
 *
 * `ScrollView` also includes logic for blurring a currently focused input
 * if one is focused while scrolling. This is a natural place
 * to put this logic since it can support not dismissing the keyboard while
 * scrolling, unless a recognized "tap"-like gesture has occurred.
 *
 * The public lifecycle API includes events for keyboard interaction, responder
 * interaction, and scrolling (among others). The keyboard callbacks
 * `onKeyboardWill/Did/*` are *global* events, but are invoked on scroll
 * responder's props so that you can guarantee that the scroll responder's
 * internal state has been updated accordingly (and deterministically) by
 * the time the props callbacks are invoke. Otherwise, you would always wonder
 * if the scroll responder is currently in a state where it recognizes new
 * keyboard positions etc. If coordinating scrolling with keyboard movement,
 * *always* use these hooks instead of listening to your own global keyboard
 * events.
 *
 * Public keyboard lifecycle API: (props callbacks)
 *
 * Standard Keyboard Appearance Sequence:
 *
 *   this.props.onKeyboardWillShow
 *   this.props.onKeyboardDidShow
 *
 * `onScrollResponderKeyboardDismissed` will be invoked if an appropriate
 * tap inside the scroll responder's scrollable region was responsible
 * for the dismissal of the keyboard. There are other reasons why the
 * keyboard could be dismissed.
 *
 *   this.props.onScrollResponderKeyboardDismissed
 *
 * Standard Keyboard Hide Sequence:
 *
 *   this.props.onKeyboardWillHide
 *   this.props.onKeyboardDidHide
 */

// Public methods for ScrollView
export type ScrollViewImperativeMethods = $ReadOnly<{|
  getScrollResponder: $PropertyType<ScrollView, "getScrollResponder">,
  getScrollableNode: $PropertyType<ScrollView, "getScrollableNode">,
  getInnerViewNode: $PropertyType<ScrollView, "getInnerViewNode">,
  getInnerViewRef: $PropertyType<ScrollView, "getInnerViewRef">,
  getNativeScrollRef: $PropertyType<ScrollView, "getNativeScrollRef">,
  scrollTo: $PropertyType<ScrollView, "scrollTo">,
  scrollToEnd: $PropertyType<ScrollView, "scrollToEnd">,
  flashScrollIndicators: $PropertyType<ScrollView, "flashScrollIndicators">,
  scrollResponderZoomTo: $PropertyType<ScrollView, "scrollResponderZoomTo">,
  scrollResponderScrollNativeHandleToKeyboard: $PropertyType<
    ScrollView,
    "scrollResponderScrollNativeHandleToKeyboard"
  >,
|}>;

export type DecelerationRateType = "fast" | "normal" | number;
export type ScrollResponderType = ScrollViewImperativeMethods;

type NativeScrollViewInstance = React.ElementRef<HostComponent<mixed>>;
type PublicScrollViewInstance = $ReadOnly<{|
  ...$Exact<NativeScrollViewInstance>,
  ...ScrollViewImperativeMethods,
|}>;

type InnerViewInstance = React.ElementRef<typeof View>;

type IOSProps = $ReadOnly<{|
  /**
   * Controls whether iOS should automatically adjust the content inset
   * for scroll views that are placed behind a navigation bar or
   * tab bar/ toolbar. The default value is true.
   * @platform ios
   */
  automaticallyAdjustContentInsets?: ?boolean,
  /**
   * Controls whether the ScrollView should automatically adjust its `contentInset`
   * and `scrollViewInsets` when the Keyboard changes its size. The default value is false.
   * @platform ios
   */
  automaticallyAdjustKeyboardInsets?: ?boolean,
  /**
   * Controls whether iOS should automatically adjust the scroll indicator
   * insets. The default value is true. Available on iOS 13 and later.
   * @platform ios
   */
  automaticallyAdjustsScrollIndicatorInsets?: ?boolean,
  /**
   * The amount by which the scroll view content is inset from the edges
   * of the scroll view. Defaults to `{top: 0, left: 0, bottom: 0, right: 0}`.
   * @platform ios
   */
  contentInset?: ?EdgeInsetsProp,
  /**
   * When true, the scroll view bounces when it reaches the end of the
   * content if the content is larger then the scroll view along the axis of
   * the scroll direction. When false, it disables all bouncing even if
   * the `alwaysBounce*` props are true. The default value is true.
   * @platform ios
   */
  bounces?: ?boolean,
  /**
   * By default, ScrollView has an active pan responder that hijacks panresponders
   * deeper in the render tree in order to prevent accidental touches while scrolling.
   * However, in certain occasions (such as when using snapToInterval) in a vertical scrollview
   * You may want to disable this behavior in order to prevent the ScrollView from blocking touches
   */
  disableScrollViewPanResponder?: ?boolean,
  /**
   * When true, gestures can drive zoom past min/max and the zoom will animate
   * to the min/max value at gesture end, otherwise the zoom will not exceed
   * the limits.
   * @platform ios
   */
  bouncesZoom?: ?boolean,
  /**
   * When true, the scroll view bounces horizontally when it reaches the end
   * even if the content is smaller than the scroll view itself. The default
   * value is true when `horizontal={true}` and false otherwise.
   * @platform ios
   */
  alwaysBounceHorizontal?: ?boolean,
  /**
   * When true, the scroll view bounces vertically when it reaches the end
   * even if the content is smaller than the scroll view itself. The default
   * value is false when `horizontal={true}` and true otherwise.
   * @platform ios
   */
  alwaysBounceVertical?: ?boolean,
  /**
   * When true, the scroll view automatically centers the content when the
   * content is smaller than the scroll view bounds; when the content is
   * larger than the scroll view, this property has no effect. The default
   * value is false.
   * @platform ios
   */
  centerContent?: ?boolean,
  /**
   * The style of the scroll indicators.
   *
   *   - `'default'` (the default), same as `black`.
   *   - `'black'`, scroll indicator is black. This style is good against a light background.
   *   - `'white'`, scroll indicator is white. This style is good against a dark background.
   *
   * @platform ios
   */
  indicatorStyle?: ?("default" | "black" | "white"),
  /**
   * When true, the ScrollView will try to lock to only vertical or horizontal
   * scrolling while dragging.  The default value is false.
   * @platform ios
   */
  directionalLockEnabled?: ?boolean,
  /**
   * When false, once tracking starts, won't try to drag if the touch moves.
   * The default value is true.
   * @platform ios
   */
  canCancelContentTouches?: ?boolean,
  /**
   * When set, the scroll view will adjust the scroll position so that the first child that is
   * currently visible and at or beyond `minIndexForVisible` will not change position. This is
   * useful for lists that are loading content in both directions, e.g. a chat thread, where new
   * messages coming in might otherwise cause the scroll position to jump. A value of 0 is common,
   * but other values such as 1 can be used to skip loading spinners or other content that should
   * not maintain position.
   *
   * The optional `autoscrollToTopThreshold` can be used to make the content automatically scroll
   * to the top after making the adjustment if the user was within the threshold of the top before
   * the adjustment was made. This is also useful for chat-like applications where you want to see
   * new messages scroll into place, but not if the user has scrolled up a ways and it would be
   * disruptive to scroll a bunch.
   *
   * Caveat 1: Reordering elements in the scrollview with this enabled will probably cause
   * jumpiness and jank. It can be fixed, but there are currently no plans to do so. For now,
   * don't re-order the content of any ScrollViews or Lists that use this feature.
   *
   * Caveat 2: This simply uses `contentOffset` and `frame.origin` in native code to compute
   * visibility. Occlusion, transforms, and other complexity won't be taken into account as to
   * whether content is "visible" or not.
   *
   */
  maintainVisibleContentPosition?: ?$ReadOnly<{|
    minIndexForVisible: number,
    autoscrollToTopThreshold?: ?number,
  |}>,
  /**
   * The maximum allowed zoom scale. The default value is 1.0.
   * @platform ios
   */
  maximumZoomScale?: ?number,
  /**
   * The minimum allowed zoom scale. The default value is 1.0.
   * @platform ios
   */
  minimumZoomScale?: ?number,
  /**
   * When true, ScrollView allows use of pinch gestures to zoom in and out.
   * The default value is true.
   * @platform ios
   */
  pinchGestureEnabled?: ?boolean,
  /**
   * The amount by which the scroll view indicators are inset from the edges
   * of the scroll view. This should normally be set to the same value as
   * the `contentInset`. Defaults to `{0, 0, 0, 0}`.
   * @platform ios
   */
  scrollIndicatorInsets?: ?EdgeInsetsProp,
  /**
   * When true, the scroll view can be programmatically scrolled beyond its
   * content size. The default value is false.
   * @platform ios
   */
  scrollToOverflowEnabled?: ?boolean,
  /**
   * When true, the scroll view scrolls to top when the status bar is tapped.
   * The default value is true.
   * @platform ios
   */
  scrollsToTop?: ?boolean,
  /**
   * Fires when the scroll view scrolls to top after the status bar has been tapped
   * @platform ios
   */
  onScrollToTop?: (event: ScrollEvent) => void,
  /**
   * When true, shows a horizontal scroll indicator.
   * The default value is true.
   */
  showsHorizontalScrollIndicator?: ?boolean,
  /**
   * The current scale of the scroll view content. The default value is 1.0.
   * @platform ios
   */
  zoomScale?: ?number,
  /**
   * This property specifies how the safe area insets are used to modify the
   * content area of the scroll view. The default value of this property is
   * "never". Available on iOS 11 and later.
   * @platform ios
   */
  contentInsetAdjustmentBehavior?: ?(
    | "automatic"
    | "scrollableAxes"
    | "never"
    | "always"
  ),
|}>;

type AndroidProps = $ReadOnly<{|
  /**
   * Enables nested scrolling for Android API level 21+.
   * Nested scrolling is supported by default on iOS
   * @platform android
   */
  nestedScrollEnabled?: ?boolean,
  /**
   * Sometimes a scrollview takes up more space than its content fills. When this is
   * the case, this prop will fill the rest of the scrollview with a color to avoid setting
   * a background and creating unnecessary overdraw. This is an advanced optimization
   * that is not needed in the general case.
   * @platform android
   */
  endFillColor?: ?ColorValue,
  /**
   * Tag used to log scroll performance on this scroll view. Will force
   * momentum events to be turned on (see sendMomentumEvents). This doesn't do
   * anything out of the box and you need to implement a custom native
   * FpsListener for it to be useful.
   * @platform android
   */
  scrollPerfTag?: ?string,
  /**
   * Used to override default value of overScroll mode.
   *
   * Possible values:
   *
   *  - `'auto'` - Default value, allow a user to over-scroll
   *    this view only if the content is large enough to meaningfully scroll.
   *  - `'always'` - Always allow a user to over-scroll this view.
   *  - `'never'` - Never allow a user to over-scroll this view.
   *
   * @platform android
   */
  overScrollMode?: ?("auto" | "always" | "never"),
  /**
   * Causes the scrollbars not to turn transparent when they are not in use.
   * The default value is false.
   *
   * @platform android
   */
  persistentScrollbar?: ?boolean,
  /**
   * Fades out the edges of the scroll content.
   *
   * If the value is greater than 0, the fading edges will be set accordingly
   * to the current scroll direction and position,
   * indicating if there is more content to show.
   *
   * The default value is 0.
   *
   * @platform android
   */
  fadingEdgeLength?: ?number,
|}>;

type StickyHeaderComponentType = React.AbstractComponent<
  ScrollViewStickyHeaderProps,
  $ReadOnly<interface { setNextHeaderY: (number) => void }>
>;

export type Props = $ReadOnly<{|
  ...ViewProps,
  ...IOSProps,
  ...AndroidProps,

  /**
   * These styles will be applied to the scroll view content container which
   * wraps all of the child views. Example:
   *
   * ```
   * return (
   *   <ScrollView contentContainerStyle={styles.contentContainer}>
   *   </ScrollView>
   * );
   * ...
   * const styles = StyleSheet.create({
   *   contentContainer: {
   *     paddingVertical: 20
   *   }
   * });
   * ```
   */
  contentContainerStyle?: ?ViewStyleProp,
  /**
   * Used to manually set the starting scroll offset.
   * The default value is `{x: 0, y: 0}`.
   */
  contentOffset?: ?PointProp,
  /**
   * When true, the scroll view stops on the next index (in relation to scroll
   * position at release) regardless of how fast the gesture is. This can be
   * used for pagination when the page is less than the width of the
   * horizontal ScrollView or the height of the vertical ScrollView. The default value is false.
   */
  disableIntervalMomentum?: ?boolean,
  /**
   * A floating-point number that determines how quickly the scroll view
   * decelerates after the user lifts their finger. You may also use string
   * shortcuts `"normal"` and `"fast"` which match the underlying iOS settings
   * for `UIScrollViewDecelerationRateNormal` and
   * `UIScrollViewDecelerationRateFast` respectively.
   *
   *   - `'normal'`: 0.998 on iOS, 0.985 on Android (the default)
   *   - `'fast'`: 0.99 on iOS, 0.9 on Android
   */
  decelerationRate?: ?DecelerationRateType,
  /**
   * When true, the scroll view's children are arranged horizontally in a row
   * instead of vertically in a column. The default value is false.
   */
  horizontal?: ?boolean,
  /**
   * If sticky headers should stick at the bottom instead of the top of the
   * ScrollView. This is usually used with inverted ScrollViews.
   */
  invertStickyHeaders?: ?boolean,
  /**
   * Determines whether the keyboard gets dismissed in response to a drag.
   *
   * *Cross platform*
   *
   *   - `'none'` (the default), drags do not dismiss the keyboard.
   *   - `'on-drag'`, the keyboard is dismissed when a drag begins.
   *
   * *iOS Only*
   *
   *   - `'interactive'`, the keyboard is dismissed interactively with the drag and moves in
   *     synchrony with the touch; dragging upwards cancels the dismissal.
   *     On android this is not supported and it will have the same behavior as 'none'.
   */
  keyboardDismissMode?: ?// default
  // cross-platform
  ("none" | "on-drag" | "interactive"), // ios only
  /**
   * Determines when the keyboard should stay visible after a tap.
   *
   *   - `'never'` (the default), tapping outside of the focused text input when the keyboard
   *     is up dismisses the keyboard. When this happens, children won't receive the tap.
   *   - `'always'`, the keyboard will not dismiss automatically, and the scroll view will not
   *     catch taps, but children of the scroll view can catch taps.
   *   - `'handled'`, the keyboard will not dismiss automatically when the tap was handled by
   *     a children, (or captured by an ancestor).
   *   - `false`, deprecated, use 'never' instead
   *   - `true`, deprecated, use 'always' instead
   */
  keyboardShouldPersistTaps?: ?("always" | "never" | "handled" | true | false),
  /**
   * Called when the momentum scroll starts (scroll which occurs as the ScrollView glides to a stop).
   */
  onMomentumScrollBegin?: ?(event: ScrollEvent) => void,
  /**
   * Called when the momentum scroll ends (scroll which occurs as the ScrollView glides to a stop).
   */
  onMomentumScrollEnd?: ?(event: ScrollEvent) => void,

  /**
   * Fires at most once per frame during scrolling. The frequency of the
   * events can be controlled using the `scrollEventThrottle` prop.
   */
  onScroll?: ?(event: ScrollEvent) => void,
  /**
   * Called when the user begins to drag the scroll view.
   */
  onScrollBeginDrag?: ?(event: ScrollEvent) => void,
  /**
   * Called when the user stops dragging the scroll view and it either stops
   * or begins to glide.
   */
  onScrollEndDrag?: ?(event: ScrollEvent) => void,
  /**
   * Called when scrollable content view of the ScrollView changes.
   *
   * Handler function is passed the content width and content height as parameters:
   * `(contentWidth, contentHeight)`
   *
   * It's implemented using onLayout handler attached to the content container
   * which this ScrollView renders.
   */
  onContentSizeChange?: (contentWidth: number, contentHeight: number) => void,
  onKeyboardDidShow?: (event: KeyboardEvent) => void,
  onKeyboardDidHide?: (event: KeyboardEvent) => void,
  onKeyboardWillShow?: (event: KeyboardEvent) => void,
  onKeyboardWillHide?: (event: KeyboardEvent) => void,
  /**
   * When true, the scroll view stops on multiples of the scroll view's size
   * when scrolling. This can be used for horizontal pagination. The default
   * value is false.
   *
   * Note: Vertical pagination is not supported on Android.
   */
  pagingEnabled?: ?boolean,
  /**
   * When false, the view cannot be scrolled via touch interaction.
   * The default value is true.
   *
   * Note that the view can always be scrolled by calling `scrollTo`.
   */
  scrollEnabled?: ?boolean,
  /**
   * This controls how often the scroll event will be fired while scrolling
   * (as a time interval in ms). A lower number yields better accuracy for code
   * that is tracking the scroll position, but can lead to scroll performance
   * problems due to the volume of information being send over the bridge.
   *
   * Values between 0 and 17ms indicate 60fps updates are needed and throttling
   * will be disabled.
   *
   * If you do not need precise scroll position tracking, set this value higher
   * to limit the information being sent across the bridge.
   *
   * The default value is zero, which results in the scroll event being sent only
   * once each time the view is scrolled.
   */
  scrollEventThrottle?: ?number,
  /**
   * When true, shows a vertical scroll indicator.
   * The default value is true.
   */
  showsVerticalScrollIndicator?: ?boolean,
  /**
   * When true, Sticky header is hidden when scrolling down, and dock at the top
   * when scrolling up
   */
  stickyHeaderHiddenOnScroll?: ?boolean,
  /**
   * An array of child indices determining which children get docked to the
   * top of the screen when scrolling. For example, passing
   * `stickyHeaderIndices={[0]}` will cause the first child to be fixed to the
   * top of the scroll view. This property is not supported in conjunction
   * with `horizontal={true}`.
   */
  stickyHeaderIndices?: ?$ReadOnlyArray<number>,
  /**
   * A React Component that will be used to render sticky headers.
   * To be used together with `stickyHeaderIndices` or with `SectionList`, defaults to `ScrollViewStickyHeader`.
   * You may need to set this if your sticky header uses custom transforms (eg. translation),
   * for example when you want your list to have an animated hidable header.
   */
  StickyHeaderComponent?: StickyHeaderComponentType,
  /**
   * When `snapToInterval` is set, `snapToAlignment` will define the relationship
   * of the snapping to the scroll view.
   *
   *   - `'start'` (the default) will align the snap at the left (horizontal) or top (vertical)
   *   - `'center'` will align the snap in the center
   *   - `'end'` will align the snap at the right (horizontal) or bottom (vertical)
   */
  snapToAlignment?: ?("start" | "center" | "end"),
  /**
   * When set, causes the scroll view to stop at multiples of the value of
   * `snapToInterval`. This can be used for paginating through children
   * that have lengths smaller than the scroll view. Typically used in
   * combination with `snapToAlignment` and `decelerationRate="fast"`.
   *
   * Overrides less configurable `pagingEnabled` prop.
   */
  snapToInterval?: ?number,
  /**
   * When set, causes the scroll view to stop at the defined offsets.
   * This can be used for paginating through variously sized children
   * that have lengths smaller than the scroll view. Typically used in
   * combination with `decelerationRate="fast"`.
   *
   * Overrides less configurable `pagingEnabled` and `snapToInterval` props.
   */
  snapToOffsets?: ?$ReadOnlyArray<number>,
  /**
   * Use in conjunction with `snapToOffsets`. By default, the beginning
   * of the list counts as a snap offset. Set `snapToStart` to false to disable
   * this behavior and allow the list to scroll freely between its start and
   * the first `snapToOffsets` offset.
   * The default value is true.
   */
  snapToStart?: ?boolean,
  /**
   * Use in conjunction with `snapToOffsets`. By default, the end
   * of the list counts as a snap offset. Set `snapToEnd` to false to disable
   * this behavior and allow the list to scroll freely between its end and
   * the last `snapToOffsets` offset.
   * The default value is true.
   */
  snapToEnd?: ?boolean,
  /**
   * Experimental: When true, offscreen child views (whose `overflow` value is
   * `hidden`) are removed from their native backing superview when offscreen.
   * This can improve scrolling performance on long lists. The default value is
   * true.
   */
  removeClippedSubviews?: ?boolean,
  /**
   * A RefreshControl component, used to provide pull-to-refresh
   * functionality for the ScrollView. Only works for vertical ScrollViews
   * (`horizontal` prop must be `false`).
   *
   * See [RefreshControl](docs/refreshcontrol.html).
   */
  /* $FlowFixMe[unclear-type] - how to handle generic type without existential
   * operator? */
  refreshControl?: ?React.Element<any>,
  children?: React.Node,
  /**
   * A ref to the inner View element of the ScrollView. This should be used
   * instead of calling `getInnerViewRef`.
   */
  innerViewRef?: ForwardedRef<InnerViewInstance>,
  /**
   * A ref to the Native ScrollView component. This ref can be used to call
   * all of ScrollView's public methods, in addition to native methods like
   * measure, measureLayout, etc.
   */
  scrollViewRef?: ForwardedRef<PublicScrollViewInstance>,
|}>;

type State = {|
  layoutHeight: ?number,
  // RNOH patch - resolving flashScrollIndicators command on the JS side
  // as it is currently not feasible on the native side
  showScrollIndicator: boolean,
|};

const IS_ANIMATING_TOUCH_START_THRESHOLD_MS = 16;

export type ScrollViewComponentStatics = $ReadOnly<{|
  Context: typeof ScrollViewContext,
|}>;

/**
 * Component that wraps platform ScrollView while providing
 * integration with touch locking "responder" system.
 *
 * Keep in mind that ScrollViews must have a bounded height in order to work,
 * since they contain unbounded-height children into a bounded container (via
 * a scroll interaction). In order to bound the height of a ScrollView, either
 * set the height of the view directly (discouraged) or make sure all parent
 * views have bounded height. Forgetting to transfer `{flex: 1}` down the
 * view stack can lead to errors here, which the element inspector makes
 * easy to debug.
 *
 * Doesn't yet support other contained responders from blocking this scroll
 * view from becoming the responder.
 *
 *
 * `<ScrollView>` vs [`<FlatList>`](https://reactnative.dev/docs/flatlist) - which one to use?
 *
 * `ScrollView` simply renders all its react child components at once. That
 * makes it very easy to understand and use.
 *
 * On the other hand, this has a performance downside. Imagine you have a very
 * long list of items you want to display, maybe several screens worth of
 * content. Creating JS components and native views for everything all at once,
 * much of which may not even be shown, will contribute to slow rendering and
 * increased memory usage.
 *
 * This is where `FlatList` comes into play. `FlatList` renders items lazily,
 * just when they are about to appear, and removes items that scroll way off
 * screen to save memory and processing time.
 *
 * `FlatList` is also handy if you want to render separators between your items,
 * multiple columns, infinite scroll loading, or any number of other features it
 * supports out of the box.
 */
class MjScrollView extends React.Component<Props, State> {
  static Context: typeof ScrollViewContext = ScrollViewContext;

  constructor(props: Props) {
    super(props);

    this._scrollAnimatedValue = new AnimatedImplementation.Value(
      this.props.contentOffset?.y ?? 0
    );
    this._scrollAnimatedValue.setOffset(this.props.contentInset?.top ?? 0);
  }

  _scrollAnimatedValue: AnimatedImplementation.Value;
  _scrollAnimatedValueAttachment: ?{ detach: () => void, ... } = null;
  _stickyHeaderRefs: Map<string, React.ElementRef<StickyHeaderComponentType>> =
    new Map();
  _headerLayoutYs: Map<string, number> = new Map();

  _keyboardMetrics: ?KeyboardMetrics = null;
  _additionalScrollOffset: number = 0;
  _isTouching: boolean = false;
  _lastMomentumScrollBeginTime: number = 0;
  _lastMomentumScrollEndTime: number = 0;

  // Reset to false every time becomes responder. This is used to:
  // - Determine if the scroll view has been scrolled and therefore should
  // refuse to give up its responder lock.
  // - Determine if releasing should dismiss the keyboard when we are in
  // tap-to-dismiss mode (this.props.keyboardShouldPersistTaps !== 'always').
  _observedScrollSinceBecomingResponder: boolean = false;
  _becameResponderWhileAnimating: boolean = false;
  _preventNegativeScrollOffset: ?boolean = null;

  _animated: ?boolean = null;

  _subscriptionKeyboardWillShow: ?EventSubscription = null;
  _subscriptionKeyboardWillHide: ?EventSubscription = null;
  _subscriptionKeyboardDidShow: ?EventSubscription = null;
  _subscriptionKeyboardDidHide: ?EventSubscription = null;

  state: State = {
    layoutHeight: null,
    // RNOH patch - resolving flashScrollIndicators command on the JS side
    // as it is currently not feasible on the native side
    showScrollIndicator: false,
  };

  componentDidMount() {
    if (typeof this.props.keyboardShouldPersistTaps === "boolean") {
      console.warn(
        `'keyboardShouldPersistTaps={${
          this.props.keyboardShouldPersistTaps === true ? "true" : "false"
        }}' is deprecated. ` +
          `Use 'keyboardShouldPersistTaps="${
            this.props.keyboardShouldPersistTaps ? "always" : "never"
          }"' instead`
      );
    }

    this._keyboardMetrics = Keyboard.metrics();
    this._additionalScrollOffset = 0;

    this._subscriptionKeyboardWillShow = Keyboard.addListener(
      "keyboardWillShow",
      this.scrollResponderKeyboardWillShow
    );
    this._subscriptionKeyboardWillHide = Keyboard.addListener(
      "keyboardWillHide",
      this.scrollResponderKeyboardWillHide
    );
    this._subscriptionKeyboardDidShow = Keyboard.addListener(
      "keyboardDidShow",
      this.scrollResponderKeyboardDidShow
    );
    this._subscriptionKeyboardDidHide = Keyboard.addListener(
      "keyboardDidHide",
      this.scrollResponderKeyboardDidHide
    );

    this._updateAnimatedNodeAttachment();
  }

  componentDidUpdate(prevProps: Props) {
    const prevContentInsetTop = prevProps.contentInset
      ? prevProps.contentInset.top
      : 0;
    const newContentInsetTop = this.props.contentInset
      ? this.props.contentInset.top
      : 0;
    if (prevContentInsetTop !== newContentInsetTop) {
      this._scrollAnimatedValue.setOffset(newContentInsetTop || 0);
    }

    this._updateAnimatedNodeAttachment();
  }

  componentWillUnmount() {
    if (this._subscriptionKeyboardWillShow != null) {
      this._subscriptionKeyboardWillShow.remove();
    }
    if (this._subscriptionKeyboardWillHide != null) {
      this._subscriptionKeyboardWillHide.remove();
    }
    if (this._subscriptionKeyboardDidShow != null) {
      this._subscriptionKeyboardDidShow.remove();
    }
    if (this._subscriptionKeyboardDidHide != null) {
      this._subscriptionKeyboardDidHide.remove();
    }

    if (this._scrollAnimatedValueAttachment) {
      this._scrollAnimatedValueAttachment.detach();
    }
  }

  /**
   * Returns a reference to the underlying scroll responder, which supports
   * operations like `scrollTo`. All ScrollView-like components should
   * implement this method so that they can be composed while providing access
   * to the underlying scroll responder's methods.
   */
  getScrollResponder: () => ScrollResponderType = () => {
    // $FlowFixMe[unclear-type]
    return ((this: any): ScrollResponderType);
  };

  getScrollableNode: () => ?number = () => {
    return findNodeHandle(this._scrollView.nativeInstance);
  };

  getInnerViewNode: () => ?number = () => {
    return findNodeHandle(this._innerView.nativeInstance);
  };

  getInnerViewRef: () => InnerViewInstance | null = () => {
    return this._innerView.nativeInstance;
  };

  getNativeScrollRef: () => NativeScrollViewInstance | null = () => {
    return this._scrollView.nativeInstance;
  };

  /**
   * Scrolls to a given x, y offset, either immediately or with a smooth animation.
   *
   * Example:
   *
   * `scrollTo({x: 0, y: 0, animated: true})`
   *
   * Note: The weird function signature is due to the fact that, for historical reasons,
   * the function also accepts separate arguments as an alternative to the options object.
   * This is deprecated due to ambiguity (y before x), and SHOULD NOT BE USED.
   */
  scrollTo: (
    options?:
      | {
          x?: number,
          y?: number,
          animated?: boolean,
          ...
        }
      | number,
    deprecatedX?: number,
    deprecatedAnimated?: boolean
  ) => void = (
    options?:
      | {
          x?: number,
          y?: number,
          animated?: boolean,
          ...
        }
      | number,
    deprecatedX?: number,
    deprecatedAnimated?: boolean
  ) => {
    let x, y, animated;
    if (typeof options === "number") {
      console.warn(
        "`scrollTo(y, x, animated)` is deprecated. Use `scrollTo({x: 5, y: 5, " +
          "animated: true})` instead."
      );
      y = options;
      x = deprecatedX;
      animated = deprecatedAnimated;
    } else if (options) {
      y = options.y;
      x = options.x;
      animated = options.animated;
    }
    if (this._scrollView.nativeInstance == null) {
      return;
    }
    Commands.scrollTo(
      this._scrollView.nativeInstance,
      x || 0,
      y || 0,
      animated !== false
    );
  };

  /**
   * If this is a vertical ScrollView scrolls to the bottom.
   * If this is a horizontal ScrollView scrolls to the right.
   *
   * Use `scrollToEnd({animated: true})` for smooth animated scrolling,
   * `scrollToEnd({animated: false})` for immediate scrolling.
   * If no options are passed, `animated` defaults to true.
   */
  scrollToEnd: (options?: ?{ animated?: boolean, ... }) => void = (
    options?: ?{ animated?: boolean, ... }
  ) => {
    // Default to true
    const animated = (options && options.animated) !== false;
    if (this._scrollView.nativeInstance == null) {
      return;
    }
    Commands.scrollToEnd(this._scrollView.nativeInstance, animated);
  };

  /**
   * Displays the scroll indicators momentarily.
   *
   * @platform ios
   */
  flashScrollIndicators: () => void = () => {
    // RNOH patch - resolving flashScrollIndicators command on the JS side
    // as it is currently not feasible on the native side

    // if (this._scrollView.nativeInstance == null) {
    //   return;
    // }
    // Commands.flashScrollIndicators(this._scrollView.nativeInstance);

    this.state.showScrollIndicator = true;
    setTimeout(() => {
      this.state.showScrollIndicator = false;
      this.forceUpdate();
    }, 500);
    this.forceUpdate();
  };

  /**
   * This method should be used as the callback to onFocus in a TextInputs'
   * parent view. Note that any module using this mixin needs to return
   * the parent view's ref in getScrollViewRef() in order to use this method.
   * @param {number} nodeHandle The TextInput node handle
   * @param {number} additionalOffset The scroll view's bottom "contentInset".
   *        Default is 0.
   * @param {bool} preventNegativeScrolling Whether to allow pulling the content
   *        down to make it meet the keyboard's top. Default is false.
   */
  scrollResponderScrollNativeHandleToKeyboard: <T>(
    nodeHandle: number | React.ElementRef<HostComponent<T>>,
    additionalOffset?: number,
    preventNegativeScrollOffset?: boolean
  ) => void = <T>(
    nodeHandle: number | React.ElementRef<HostComponent<T>>,
    additionalOffset?: number,
    preventNegativeScrollOffset?: boolean
  ) => {
    this._additionalScrollOffset = additionalOffset || 0;
    this._preventNegativeScrollOffset = !!preventNegativeScrollOffset;

    if (this._innerView.nativeInstance == null) {
      return;
    }

    if (typeof nodeHandle === "number") {
      UIManager.measureLayout(
        nodeHandle,
        nullthrows(findNodeHandle(this)),
        // $FlowFixMe[method-unbinding] added when improving typing for this parameters
        this._textInputFocusError,
        this._inputMeasureAndScrollToKeyboard
      );
    } else {
      nodeHandle.measureLayout(
        this._innerView.nativeInstance,
        this._inputMeasureAndScrollToKeyboard,
        // $FlowFixMe[method-unbinding] added when improving typing for this parameters
        this._textInputFocusError
      );
    }
  };

  /**
   * A helper function to zoom to a specific rect in the scrollview. The argument has the shape
   * {x: number; y: number; width: number; height: number; animated: boolean = true}
   *
   * @platform ios
   */
  scrollResponderZoomTo: (
    rect: {|
      x: number,
      y: number,
      width: number,
      height: number,
      animated?: boolean,
    |},
    animated?: boolean // deprecated, put this inside the rect argument instead
  ) => void = (
    rect: {|
      x: number,
      y: number,
      width: number,
      height: number,
      animated?: boolean,
    |},
    animated?: boolean // deprecated, put this inside the rect argument instead
  ) => {
    invariant(Platform.OS === "ios", "zoomToRect is not implemented");
    if ("animated" in rect) {
      this._animated = rect.animated;
      delete rect.animated;
    } else if (typeof animated !== "undefined") {
      console.warn(
        "`scrollResponderZoomTo` `animated` argument is deprecated. Use `options.animated` instead"
      );
    }

    if (this._scrollView.nativeInstance == null) {
      return;
    }
    Commands.zoomToRect(
      this._scrollView.nativeInstance,
      rect,
      animated !== false
    );
  };

  _textInputFocusError() {
    console.warn("Error measuring text field.");
  }

  /**
   * The calculations performed here assume the scroll view takes up the entire
   * screen - even if has some content inset. We then measure the offsets of the
   * keyboard, and compensate both for the scroll view's "contentInset".
   *
   * @param {number} left Position of input w.r.t. table view.
   * @param {number} top Position of input w.r.t. table view.
   * @param {number} width Width of the text input.
   * @param {number} height Height of the text input.
   */
  _inputMeasureAndScrollToKeyboard: (
    left: number,
    top: number,
    width: number,
    height: number
  ) => void = (left: number, top: number, width: number, height: number) => {
    let keyboardScreenY = Dimensions.get("window").height;

    const scrollTextInputIntoVisibleRect = () => {
      if (this._keyboardMetrics != null) {
        keyboardScreenY = this._keyboardMetrics.screenY;
      }
      let scrollOffsetY =
        top - keyboardScreenY + height + this._additionalScrollOffset;

      // By default, this can scroll with negative offset, pulling the content
      // down so that the target component's bottom meets the keyboard's top.
      // If requested otherwise, cap the offset at 0 minimum to avoid content
      // shifting down.
      if (this._preventNegativeScrollOffset === true) {
        scrollOffsetY = Math.max(0, scrollOffsetY);
      }
      this.scrollTo({ x: 0, y: scrollOffsetY, animated: true });

      this._additionalScrollOffset = 0;
      this._preventNegativeScrollOffset = false;
    };

    if (this._keyboardMetrics == null) {
      // `_keyboardMetrics` is set inside `scrollResponderKeyboardWillShow` which
      // is not guaranteed to be called before `_inputMeasureAndScrollToKeyboard` but native has already scheduled it.
      // In case it was not called before `_inputMeasureAndScrollToKeyboard`, we postpone scrolling to
      // text input.
      setTimeout(() => {
        scrollTextInputIntoVisibleRect();
      }, 0);
    } else {
      scrollTextInputIntoVisibleRect();
    }
  };

  _getKeyForIndex(index: $FlowFixMe, childArray: $FlowFixMe): $FlowFixMe {
    const child = childArray[index];
    return child && child.key;
  }

  _updateAnimatedNodeAttachment() {
    if (this._scrollAnimatedValueAttachment) {
      this._scrollAnimatedValueAttachment.detach();
    }
    if (
      this.props.stickyHeaderIndices &&
      this.props.stickyHeaderIndices.length > 0
    ) {
      this._scrollAnimatedValueAttachment =
        AnimatedImplementation.attachNativeEvent(
          this._scrollView.nativeInstance,
          "onScroll",
          [{ nativeEvent: { contentOffset: { y: this._scrollAnimatedValue } } }]
        );
    }
  }

  _setStickyHeaderRef(
    key: string,
    ref: ?React.ElementRef<StickyHeaderComponentType>
  ) {
    if (ref) {
      this._stickyHeaderRefs.set(key, ref);
    } else {
      this._stickyHeaderRefs.delete(key);
    }
  }

  _onStickyHeaderLayout(index: $FlowFixMe, event: $FlowFixMe, key: $FlowFixMe) {
    const { stickyHeaderIndices } = this.props;
    if (!stickyHeaderIndices) {
      return;
    }
    const childArray = React.Children.toArray<$FlowFixMe>(this.props.children);
    if (key !== this._getKeyForIndex(index, childArray)) {
      // ignore stale layout update
      return;
    }

    const layoutY = event.nativeEvent.layout.y;
    this._headerLayoutYs.set(key, layoutY);

    const indexOfIndex = stickyHeaderIndices.indexOf(index);
    const previousHeaderIndex = stickyHeaderIndices[indexOfIndex - 1];
    if (previousHeaderIndex != null) {
      const previousHeader = this._stickyHeaderRefs.get(
        this._getKeyForIndex(previousHeaderIndex, childArray)
      );
      previousHeader &&
        previousHeader.setNextHeaderY &&
        previousHeader.setNextHeaderY(layoutY);
    }
  }

  _handleScroll = (e: ScrollEvent) => {
    if (__DEV__) {
      if (
        this.props.onScroll &&
        this.props.scrollEventThrottle == null &&
        Platform.OS === "ios"
      ) {
        console.log(
          "You specified `onScroll` on a <ScrollView> but not " +
            "`scrollEventThrottle`. You will only receive one event. " +
            "Using `16` you get all the events but be aware that it may " +
            "cause frame drops, use a bigger number if you don't need as " +
            "much precision."
        );
      }
    }
    this._observedScrollSinceBecomingResponder = true;
    this.props.onScroll && this.props.onScroll(e);
  };

  _handleLayout = (e: LayoutEvent) => {
    if (this.props.invertStickyHeaders === true) {
      this.setState({ layoutHeight: e.nativeEvent.layout.height });
    }
    if (this.props.onLayout) {
      this.props.onLayout(e);
    }
  };

  _handleContentOnLayout = (e: LayoutEvent) => {
    const { width, height } = e.nativeEvent.layout;
    this.props.onContentSizeChange &&
      this.props.onContentSizeChange(width, height);
  };

  _innerView: RefForwarder<InnerViewInstance, InnerViewInstance> =
    createRefForwarder(
      (instance: InnerViewInstance): InnerViewInstance => instance
    );

  _scrollView: RefForwarder<
    NativeScrollViewInstance,
    PublicScrollViewInstance
  > = createRefForwarder(
    (nativeInstance: NativeScrollViewInstance): PublicScrollViewInstance => {
      // This is a hack. Ideally we would forwardRef  to the underlying
      // host component. However, since ScrollView has it's own methods that can be
      // called as well, if we used the standard forwardRef then these
      // methods wouldn't be accessible and thus be a breaking change.
      //
      // Therefore we edit ref to include ScrollView's public methods so that
      // they are callable from the ref.

      // $FlowFixMe[prop-missing] - Known issue with appending custom methods.
      const publicInstance: PublicScrollViewInstance = Object.assign(
        nativeInstance,
        {
          getScrollResponder: this.getScrollResponder,
          getScrollableNode: this.getScrollableNode,
          getInnerViewNode: this.getInnerViewNode,
          getInnerViewRef: this.getInnerViewRef,
          getNativeScrollRef: this.getNativeScrollRef,
          scrollTo: this.scrollTo,
          scrollToEnd: this.scrollToEnd,
          flashScrollIndicators: this.flashScrollIndicators,
          scrollResponderZoomTo: this.scrollResponderZoomTo,
          scrollResponderScrollNativeHandleToKeyboard:
            this.scrollResponderScrollNativeHandleToKeyboard,
        }
      );

      return publicInstance;
    }
  );

  /**
   * Warning, this may be called several times for a single keyboard opening.
   * It's best to store the information in this method and then take any action
   * at a later point (either in `keyboardDidShow` or other).
   *
   * Here's the order that events occur in:
   * - focus
   * - willShow {startCoordinates, endCoordinates} several times
   * - didShow several times
   * - blur
   * - willHide {startCoordinates, endCoordinates} several times
   * - didHide several times
   *
   * The `ScrollResponder` module callbacks for each of these events.
   * Even though any user could have easily listened to keyboard events
   * themselves, using these `props` callbacks ensures that ordering of events
   * is consistent - and not dependent on the order that the keyboard events are
   * subscribed to. This matters when telling the scroll view to scroll to where
   * the keyboard is headed - the scroll responder better have been notified of
   * the keyboard destination before being instructed to scroll to where the
   * keyboard will be. Stick to the `ScrollResponder` callbacks, and everything
   * will work.
   *
   * WARNING: These callbacks will fire even if a keyboard is displayed in a
   * different navigation pane. Filter out the events to determine if they are
   * relevant to you. (For example, only if you receive these callbacks after
   * you had explicitly focused a node etc).
   */

  scrollResponderKeyboardWillShow: (e: KeyboardEvent) => void = (
    e: KeyboardEvent
  ) => {
    this._keyboardMetrics = e.endCoordinates;
    this.props.onKeyboardWillShow && this.props.onKeyboardWillShow(e);
  };

  scrollResponderKeyboardWillHide: (e: KeyboardEvent) => void = (
    e: KeyboardEvent
  ) => {
    this._keyboardMetrics = null;
    this.props.onKeyboardWillHide && this.props.onKeyboardWillHide(e);
  };

  scrollResponderKeyboardDidShow: (e: KeyboardEvent) => void = (
    e: KeyboardEvent
  ) => {
    this._keyboardMetrics = e.endCoordinates;
    this.props.onKeyboardDidShow && this.props.onKeyboardDidShow(e);
  };

  scrollResponderKeyboardDidHide: (e: KeyboardEvent) => void = (
    e: KeyboardEvent
  ) => {
    this._keyboardMetrics = null;
    this.props.onKeyboardDidHide && this.props.onKeyboardDidHide(e);
  };

  /**
   * Invoke this from an `onMomentumScrollBegin` event.
   */
  _handleMomentumScrollBegin: (e: ScrollEvent) => void = (e: ScrollEvent) => {
    this._lastMomentumScrollBeginTime = global.performance.now();
    this.props.onMomentumScrollBegin && this.props.onMomentumScrollBegin(e);
  };

  /**
   * Invoke this from an `onMomentumScrollEnd` event.
   */
  _handleMomentumScrollEnd: (e: ScrollEvent) => void = (e: ScrollEvent) => {
    FrameRateLogger.endScroll();
    this._lastMomentumScrollEndTime = global.performance.now();
    this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e);
  };

  /**
   * Unfortunately, `onScrollBeginDrag` also fires when *stopping* the scroll
   * animation, and there's not an easy way to distinguish a drag vs. stopping
   * momentum.
   *
   * Invoke this from an `onScrollBeginDrag` event.
   */
  _handleScrollBeginDrag: (e: ScrollEvent) => void = (e: ScrollEvent) => {
    FrameRateLogger.beginScroll(); // TODO: track all scrolls after implementing onScrollEndAnimation

    if (
      // RNOH: patch - add support for OpenHarmony
      (Platform.OS === "android" || Platform.OS === "harmony") &&
      this.props.keyboardDismissMode === "on-drag"
    ) {
      dismissKeyboard();
    }

    this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e);
  };

  /**
   * Invoke this from an `onScrollEndDrag` event.
   */
  _handleScrollEndDrag: (e: ScrollEvent) => void = (e: ScrollEvent) => {
    const { velocity } = e.nativeEvent;
    // - If we are animating, then this is a "drag" that is stopping the scrollview and momentum end
    //   will fire.
    // - If velocity is non-zero, then the interaction will stop when momentum scroll ends or
    //   another drag starts and ends.
    // - If we don't get velocity, better to stop the interaction twice than not stop it.
    if (
      !this._isAnimating() &&
      (!velocity || (velocity.x === 0 && velocity.y === 0))
    ) {
      FrameRateLogger.endScroll();
    }
    this.props.onScrollEndDrag && this.props.onScrollEndDrag(e);
  };

  /**
   * A helper function for this class that lets us quickly determine if the
   * view is currently animating. This is particularly useful to know when
   * a touch has just started or ended.
   */
  _isAnimating: () => boolean = () => {
    const now = global.performance.now();
    const timeSinceLastMomentumScrollEnd =
      now - this._lastMomentumScrollEndTime;
    const isAnimating =
      timeSinceLastMomentumScrollEnd < IS_ANIMATING_TOUCH_START_THRESHOLD_MS ||
      this._lastMomentumScrollEndTime < this._lastMomentumScrollBeginTime;
    return isAnimating;
  };

  /**
   * Invoke this from an `onResponderGrant` event.
   */
  _handleResponderGrant: (e: PressEvent) => void = (e: PressEvent) => {
    this._observedScrollSinceBecomingResponder = false;
    this.props.onResponderGrant && this.props.onResponderGrant(e);
    this._becameResponderWhileAnimating = this._isAnimating();
  };

  /**
   * Invoke this from an `onResponderReject` event.
   *
   * Some other element is not yielding its role as responder. Normally, we'd
   * just disable the `UIScrollView`, but a touch has already began on it, the
   * `UIScrollView` will not accept being disabled after that. The easiest
   * solution for now is to accept the limitation of disallowing this
   * altogether. To improve this, find a way to disable the `UIScrollView` after
   * a touch has already started.
   */
  _handleResponderReject: () => void = () => {};

  /**
   * Invoke this from an `onResponderRelease` event.
   */
  _handleResponderRelease: (e: PressEvent) => void = (e: PressEvent) => {
    this._isTouching = e.nativeEvent.touches.length !== 0;
    this.props.onResponderRelease && this.props.onResponderRelease(e);

    if (typeof e.target === "number") {
      if (__DEV__) {
        console.error(
          "Did not expect event target to be a number. Should have been a native component"
        );
      }

      return;
    }

    // By default scroll views will unfocus a textField
    // if another touch occurs outside of it
    const currentlyFocusedTextInput = TextInputState.currentlyFocusedInput();
    if (
      currentlyFocusedTextInput != null &&
      this.props.keyboardShouldPersistTaps !== true &&
      this.props.keyboardShouldPersistTaps !== "always" &&
      this._keyboardIsDismissible() &&
      e.target !== currentlyFocusedTextInput &&
      !this._observedScrollSinceBecomingResponder &&
      !this._becameResponderWhileAnimating
    ) {
      TextInputState.blurTextInput(currentlyFocusedTextInput);
    }
  };

  /**
   * We will allow the scroll view to give up its lock iff it acquired the lock
   * during an animation. This is a very useful default that happens to satisfy
   * many common user experiences.
   *
   * - Stop a scroll on the left edge, then turn that into an outer view's
   *   backswipe.
   * - Stop a scroll mid-bounce at the top, continue pulling to have the outer
   *   view dismiss.
   * - However, without catching the scroll view mid-bounce (while it is
   *   motionless), if you drag far enough for the scroll view to become
   *   responder (and therefore drag the scroll view a bit), any backswipe
   *   navigation of a swipe gesture higher in the view hierarchy, should be
   *   rejected.
   */
  _handleResponderTerminationRequest: () => boolean = () => {
    return !this._observedScrollSinceBecomingResponder;
  };

  /**
   * Invoke this from an `onScroll` event.
   */
  _handleScrollShouldSetResponder: () => boolean = () => {
    // Allow any event touch pass through if the default pan responder is disabled
    if (this.props.disableScrollViewPanResponder === true) {
      return false;
    }
    return this._isTouching;
  };

  /**
   * Merely touch starting is not sufficient for a scroll view to become the
   * responder. Being the "responder" means that the very next touch move/end
   * event will result in an action/movement.
   *
   * Invoke this from an `onStartShouldSetResponder` event.
   *
   * `onStartShouldSetResponder` is used when the next move/end will trigger
   * some UI movement/action, but when you want to yield priority to views
   * nested inside of the view.
   *
   * There may be some cases where scroll views actually should return `true`
   * from `onStartShouldSetResponder`: Any time we are detecting a standard tap
   * that gives priority to nested views.
   *
   * - If a single tap on the scroll view triggers an action such as
   *   recentering a map style view yet wants to give priority to interaction
   *   views inside (such as dropped pins or labels), then we would return true
   *   from this method when there is a single touch.
   *
   * - Similar to the previous case, if a two finger "tap" should trigger a
   *   zoom, we would check the `touches` count, and if `>= 2`, we would return
   *   true.
   *
   */
  _handleStartShouldSetResponder: (e: PressEvent) => boolean = (
    e: PressEvent
  ) => {
    // Allow any event touch pass through if the default pan responder is disabled
    if (this.props.disableScrollViewPanResponder === true) {
      return false;
    }

    const currentlyFocusedInput = TextInputState.currentlyFocusedInput();
    if (
      this.props.keyboardShouldPersistTaps === "handled" &&
      this._keyboardIsDismissible() &&
      e.target !== currentlyFocusedInput
    ) {
      return true;
    }
    return false;
  };

  /**
   * There are times when the scroll view wants to become the responder
   * (meaning respond to the next immediate `touchStart/touchEnd`), in a way
   * that *doesn't* give priority to nested views (hence the capture phase):
   *
   * - Currently animating.
   * - Tapping anywhere that is not a text input, while the keyboard is
   *   up (which should dismiss the keyboard).
   *
   * Invoke this from an `onStartShouldSetResponderCapture` event.
   */
  _handleStartShouldSetResponderCapture: (e: PressEvent) => boolean = (
    e: PressEvent
  ) => {
    // The scroll view should receive taps instead of its descendants if:
    // * it is already animating/decelerating
    if (this._isAnimating()) {
      return true;
    }

    // Allow any event touch pass through if the default pan responder is disabled
    if (this.props.disableScrollViewPanResponder === true) {
      return false;
    }

    // * the keyboard is up, keyboardShouldPersistTaps is 'never' (the default),
    // and a new touch starts with a non-textinput target (in which case the
    // first tap should be sent to the scroll view and dismiss the keyboard,
    // then the second tap goes to the actual interior view)
    const { keyboardShouldPersistTaps } = this.props;
    const keyboardNeverPersistTaps =
      !keyboardShouldPersistTaps || keyboardShouldPersistTaps === "never";

    if (typeof e.target === "number") {
      if (__DEV__) {
        console.error(
          "Did not expect event target to be a number. Should have been a native component"
        );
      }

      return false;
    }

    // Let presses through if the soft keyboard is detached from the viewport
    if (this._softKeyboardIsDetached()) {
      return false;
    }

    if (
      keyboardNeverPersistTaps &&
      this._keyboardIsDismissible() &&
      e.target != null &&
      // $FlowFixMe[incompatible-call]
      !TextInputState.isTextInput(e.target)
    ) {
      return true;
    }

    return false;
  };

  /**
   * Do we consider there to be a dismissible soft-keyboard open?
   */
  _keyboardIsDismissible: () => boolean = () => {
    const currentlyFocusedInput = TextInputState.currentlyFocusedInput();

    // We cannot dismiss the keyboard without an input to blur, even if a soft
    // keyboard is open (e.g. when keyboard is open due to a native component
    // not participating in TextInputState). It's also possible that the
    // currently focused input isn't a TextInput (such as by calling ref.focus
    // on a non-TextInput).
    const hasFocusedTextInput =
      currentlyFocusedInput != null &&
      TextInputState.isTextInput(currentlyFocusedInput);

    // Even if an input is focused, we may not have a keyboard to dismiss. E.g
    // when using a physical keyboard. Ensure we have an event for an opened
    // keyboard.
    const softKeyboardMayBeOpen =
      this._keyboardMetrics != null || this._keyboardEventsAreUnreliable();

    return hasFocusedTextInput && softKeyboardMayBeOpen;
  };

  /**
   * Whether an open soft keyboard is present which does not overlap the
   * viewport. E.g. for a VR soft-keyboard which is detached from the app
   * viewport.
   */
  _softKeyboardIsDetached: () => boolean = () => {
    return this._keyboardMetrics != null && this._keyboardMetrics.height === 0;
  };

  _keyboardEventsAreUnreliable: () => boolean = () => {
    // Android versions prior to API 30 rely on observing layout changes when
    // `android:windowSoftInputMode` is set to `adjustResize` or `adjustPan`.
    return Platform.OS === "android" && Platform.Version < 30;
  };

  /**
   * Invoke this from an `onTouchEnd` event.
   *
   * @param {PressEvent} e Event.
   */
  _handleTouchEnd: (e: PressEvent) => void = (e: PressEvent) => {
    const nativeEvent = e.nativeEvent;
    this._isTouching = nativeEvent.touches.length !== 0;

    const { keyboardShouldPersistTaps } = this.props;
    const keyboardNeverPersistsTaps =
      !keyboardShouldPersistTaps || keyboardShouldPersistTaps === "never";

    // Dismiss the keyboard now if we didn't become responder in capture phase
    // to eat presses, but still want to dismiss on interaction.
    // Don't do anything if the target of the touch event is the current input.
    const currentlyFocusedTextInput = TextInputState.currentlyFocusedInput();
    if (
      currentlyFocusedTextInput != null &&
      e.target !== currentlyFocusedTextInput &&
      this._softKeyboardIsDetached() &&
      this._keyboardIsDismissible() &&
      keyboardNeverPersistsTaps
    ) {
      TextInputState.blurTextInput(currentlyFocusedTextInput);
    }

    this.props.onTouchEnd && this.props.onTouchEnd(e);
  };

  /**
   * Invoke this from an `onTouchCancel` event.
   *
   * @param {PressEvent} e Event.
   */
  _handleTouchCancel: (e: PressEvent) => void = (e: PressEvent) => {
    this._isTouching = false;
    this.props.onTouchCancel && this.props.onTouchCancel(e);
  };

  /**
   * Invoke this from an `onTouchStart` event.
   *
   * Since we know that the `SimpleEventPlugin` occurs later in the plugin
   * order, after `ResponderEventPlugin`, we can detect that we were *not*
   * permitted to be the responder (presumably because a contained view became
   * responder). The `onResponderReject` won't fire in that case - it only
   * fires when a *current* responder rejects our request.
   *
   * @param {PressEvent} e Touch Start event.
   */
  _handleTouchStart: (e: PressEvent) => void = (e: PressEvent) => {
    this._isTouching = true;
    this.props.onTouchStart && this.props.onTouchStart(e);
  };

  /**
   * Invoke this from an `onTouchMove` event.
   *
   * Since we know that the `SimpleEventPlugin` occurs later in the plugin
   * order, after `ResponderEventPlugin`, we can detect that we were *not*
   * permitted to be the responder (presumably because a contained view became
   * responder). The `onResponderReject` won't fire in that case - it only
   * fires when a *current* responder rejects our request.
   *
   * @param {PressEvent} e Touch Start event.
   */
  _handleTouchMove: (e: PressEvent) => void = (e: PressEvent) => {
    this.props.onTouchMove && this.props.onTouchMove(e);
  };

  render(): React.Node | React.Element<string> {
    const [NativeDirectionalScrollView, NativeDirectionalScrollContentView] =
      this.props.horizontal === true
        ? NativeHorizontalScrollViewTuple
        : NativeVerticalScrollViewTuple;

    const contentContainerStyle = [
      this.props.horizontal === true && styles.contentContainerHorizontal,
      this.props.contentContainerStyle,
    ];
    if (__DEV__ && this.props.style !== undefined) {
      // $FlowFixMe[underconstrained-implicit-instantiation]
      const style = flattenStyle(this.props.style);
      const childLayoutProps = ["alignItems", "justifyContent"].filter(
        (prop) => style && style[prop] !== undefined
      );
      invariant(
        childLayoutProps.length === 0,
        "ScrollView child layout (" +
          JSON.stringify(childLayoutProps) +
          ") must be applied through the contentContainerStyle prop."
      );
    }

    const contentSizeChangeProps =
      this.props.onContentSizeChange == null
        ? null
        : {
            onLayout: this._handleContentOnLayout,
          };

    const { stickyHeaderIndices } = this.props;
    let children = this.props.children;

    if (stickyHeaderIndices != null && stickyHeaderIndices.length > 0) {
      const childArray = React.Children.toArray<$FlowFixMe>(
        this.props.children
      );

      children = childArray.map((child, index) => {
        const indexOfIndex = child ? stickyHeaderIndices.indexOf(index) : -1;
        if (indexOfIndex > -1) {
          const key = child.key;
          const nextIndex = stickyHeaderIndices[indexOfIndex + 1];
          const StickyHeaderComponent =
            this.props.StickyHeaderComponent || ScrollViewStickyHeader;
          return (
            <StickyHeaderComponent
              key={key}
              nativeID={"StickyHeader-" + key} /* TODO: T68258846. */
              ref={(ref) => this._setStickyHeaderRef(key, ref)}
              nextHeaderLayoutY={this._headerLayoutYs.get(
                this._getKeyForIndex(nextIndex, childArray)
              )}
              onLayout={(event) =>
                this._onStickyHeaderLayout(index, event, key)
              }
              scrollAnimatedValue={this._scrollAnimatedValue}
              inverted={this.props.invertStickyHeaders}
              hiddenOnScroll={this.props.stickyHeaderHiddenOnScroll}
              scrollViewHeight={this.state.layoutHeight}
            >
              {child}
            </StickyHeaderComponent>
          );
        } else {
          return child;
        }
      });
    }
    children = (
      <ScrollViewContext.Provider
        value={this.props.horizontal === true ? HORIZONTAL : VERTICAL}
      >
        {children}
      </ScrollViewContext.Provider>
    );

    const hasStickyHeaders =
      Array.isArray(stickyHeaderIndices) && stickyHeaderIndices.length > 0;

    const contentContainer = (
      <NativeDirectionalScrollContentView
        {...contentSizeChangeProps}
        ref={this._innerView.getForwardingRef(this.props.innerViewRef)}
        style={contentContainerStyle}
        removeClippedSubviews={
          // Subview clipping causes issues with sticky headers on Android and
          // would be hard to fix properly in a performant way.
          Platform.OS === "android" && hasStickyHeaders
            ? false
            : this.props.removeClippedSubviews
        }
        collapsable={false}
      >
        {children}
      </NativeDirectionalScrollContentView>
    );

    const alwaysBounceHorizontal =
      this.props.alwaysBounceHorizontal !== undefined
        ? this.props.alwaysBounceHorizontal
        : this.props.horizontal;

    const alwaysBounceVertical =
      this.props.alwaysBounceVertical !== undefined
        ? this.props.alwaysBounceVertical
        : !this.props.horizontal;

    const baseStyle =
      this.props.horizontal === true
        ? styles.baseHorizontal
        : styles.baseVertical;
    const props = {
      ...this.props,
      alwaysBounceHorizontal,
      alwaysBounceVertical,
      style: StyleSheet.compose(baseStyle, this.props.style),
      // Override the onContentSizeChange from props, since this event can
      // bubble up from TextInputs
      onContentSizeChange: null,
      onLayout: this._handleLayout,
      onMomentumScrollBegin: this._handleMomentumScrollBegin,
      onMomentumScrollEnd: this._handleMomentumScrollEnd,
      onResponderGrant: this._handleResponderGrant,
      onResponderReject: this._handleResponderReject,
      onResponderRelease: this._handleResponderRelease,
      onResponderTerminationRequest: this._handleResponderTerminationRequest,
      onScrollBeginDrag: this._handleScrollBeginDrag,
      onScrollEndDrag: this._handleScrollEndDrag,
      onScrollShouldSetResponder: this._handleScrollShouldSetResponder,
      onStartShouldSetResponder: this._handleStartShouldSetResponder,
      onStartShouldSetResponderCapture:
        this._handleStartShouldSetResponderCapture,
      onTouchEnd: this._handleTouchEnd,
      onTouchMove: this._handleTouchMove,
      onTouchStart: this._handleTouchStart,
      onTouchCancel: this._handleTouchCancel,
      onScroll: this._handleScroll,
      scrollEventThrottle: hasStickyHeaders
        ? 1
        : this.props.scrollEventThrottle,
      sendMomentumEvents:
        this.props.onMomentumScrollBegin || this.props.onMomentumScrollEnd
          ? true
          : false,
      // default to true
      snapToStart: this.props.snapToStart !== false,
      // default to true
      snapToEnd: this.props.snapToEnd !== false,
      // pagingEnabled is overridden by snapToInterval / snapToOffsets
      pagingEnabled:
        this.props.pagingEnabled === true &&
        this.props.snapToInterval == null &&
        this.props.snapToOffsets == null,
      // RNOH patch - resolving flashScrollIndicators command on the JS side
      // as it is currently not feasible on the native side 
      showsVerticalScrollIndicator: (this.props.showsVerticalScrollIndicator ?? true) || this.state.showScrollIndicator,
      showsHorizontalScrollIndicator: (this.props.showsHorizontalScrollIndicator ?? true) || this.state.showScrollIndicator,
      persistentScrollbar: this.props.persistentScrollbar || this.state.showScrollIndicator,
    };

    const { decelerationRate } = this.props;
    if (decelerationRate != null) {
      props.decelerationRate = processDecelerationRate(decelerationRate);
    }

    const refreshControl = this.props.refreshControl;
    const scrollViewRef = this._scrollView.getForwardingRef(
      this.props.scrollViewRef
    );

    if (refreshControl) {
      // RNOH: patch - use Android implementation
      const { outer, inner } = splitLayoutProps(flattenStyle(props.style));

      const children = [
        <NativeDirectionalScrollView
          key="scrollview"
          {...props}
          style={StyleSheet.compose(baseStyle, inner)}
          ref={scrollViewRef}
        >
          {contentContainer}
        </NativeDirectionalScrollView>,
      ];

      if (refreshControl.props.children) {
        children.push(
          React.cloneElement(refreshControl.props.children, {
            key: "refresh-indicator",
            style: [
              refreshControl.props.children.props.style,
              {
                position: "absolute",
                top: 0,
                left: 0,
                right: 0,
                zIndex: 1000,
              },
            ],
          })
        );
      }

      return React.cloneElement(refreshControl, {
        style: StyleSheet.compose(baseStyle, outer),
        children: children,
      });
    }

    return (
      <NativeDirectionalScrollView {...props} ref={scrollViewRef}>
        {contentContainer}
      </NativeDirectionalScrollView>
    );
  }
}

const styles = StyleSheet.create({
  baseVertical: {
    flexGrow: 1,
    flexShrink: 1,
    flexDirection: "column",
    overflow: "scroll",
  },
  baseHorizontal: {
    flexGrow: 1,
    flexShrink: 1,
    flexDirection: "row",
    overflow: "scroll",
  },
  contentContainerHorizontal: {
    flexDirection: "row",
  },
});

type ForwardedRef<T> = { current: null | T, ... } | ((null | T) => mixed);

type RefForwarder<TNativeInstance, TPublicInstance> = {
  getForwardingRef: (
    ?ForwardedRef<TPublicInstance>
  ) => (TNativeInstance | null) => void,
  nativeInstance: TNativeInstance | null,
  publicInstance: TPublicInstance | null,
};

/**
 * Helper function that should be replaced with `useCallback` and `useMergeRefs`
 * once `ScrollView` is reimplemented as a functional component.
 */
function createRefForwarder<TNativeInstance, TPublicInstance>(
  mutator: (TNativeInstance) => TPublicInstance
): RefForwarder<TNativeInstance, TPublicInstance> {
  const state: RefForwarder<TNativeInstance, TPublicInstance> = {
    getForwardingRef: memoize((forwardedRef) => {
      return (nativeInstance: TNativeInstance | null): void => {
        const publicInstance =
          nativeInstance == null ? null : mutator(nativeInstance);

        state.nativeInstance = nativeInstance;
        state.publicInstance = publicInstance;

        if (forwardedRef != null) {
          if (typeof forwardedRef === "function") {
            forwardedRef(publicInstance);
          } else {
            forwardedRef.current = publicInstance;
          }
        }
      };
    }),
    nativeInstance: null,
    publicInstance: null,
  };

  return state;
}

module.exports = (Platform.OS === 'ios' || Platform.OS === 'harmony') ? MjScrollView : ScrollView;