/*
 * 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.
 */

#import "RCTViewComponentView.h"
#import "RCTViewAccessibilityElement.h"

#import <CoreGraphics/CoreGraphics.h>
#import <QuartzCore/QuartzCore.h>
#import <objc/runtime.h>
#import <ranges>
#if TARGET_OS_OSX // [macOS
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#endif // macOS]

#import <React/RCTAssert.h>
#import <React/RCTBorderDrawing.h>
#import <React/RCTBoxShadow.h>
#import <React/RCTConversions.h>
#import <React/RCTLinearGradient.h>
#import <React/RCTLocalizedString.h>
#import <React/RCTRadialGradient.h>
#import <react/featureflags/ReactNativeFeatureFlags.h>
#import <react/renderer/components/view/ViewComponentDescriptor.h>
#import <react/renderer/components/view/ViewEventEmitter.h>
#import <react/renderer/components/view/ViewProps.h>
#import <react/renderer/components/view/accessibilityPropsConversions.h>
#import <react/renderer/graphics/BlendMode.h>

#ifdef RCT_DYNAMIC_FRAMEWORKS
#import <React/RCTComponentViewFactory.h>
#endif

#if TARGET_OS_OSX // [macOS
#import <React/RCTCursor.h>
#import <React/RCTViewKeyboardEvent.h>
#import <React/RCTUtils.h>
#endif // macOS]

using namespace facebook::react;

const CGFloat BACKGROUND_COLOR_ZPOSITION = -1024.0f;

@implementation RCTViewComponentView {
  RCTUIColor *_backgroundColor; // [macOS]
  CALayer *_backgroundColorLayer;
  __weak CALayer *_borderLayer;
  CALayer *_outlineLayer;
  NSMutableArray<CALayer *> *_boxShadowLayers;
  CALayer *_filterLayer;
  NSMutableArray<CALayer *> *_backgroundImageLayers;
  BOOL _needsInvalidateLayer;
  BOOL _isJSResponder;
  BOOL _removeClippedSubviews;
#if TARGET_OS_OSX // [macOS
  BOOL _hasMouseOver;
  BOOL _hasClipViewBoundsObserver;
  NSTrackingArea *_trackingArea;
  BOOL _allowsVibrancy;
#endif // macOS]
  NSMutableArray<RCTUIView *> *_reactSubviews; // [macOS]
  NSSet<NSString *> *_Nullable _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN;
  RCTPlatformView *_containerView; // [macOS]
  BOOL _useCustomContainerView;
  NSMutableSet<NSString *> *_accessibilityOrderNativeIDs;
  NSMutableArray<NSObject *> *_accessibilityElements;
  RCTViewAccessibilityElement *_axElementDescribingSelf;
}

#ifdef RCT_DYNAMIC_FRAMEWORKS
+ (void)load
{
  [RCTComponentViewFactory.currentComponentViewFactory registerComponentViewClass:self];
}
#endif

- (instancetype)initWithFrame:(CGRect)frame
{
  if (self = [super initWithFrame:frame]) {
    _props = ViewShadowNode::defaultSharedProps();
    _reactSubviews = [NSMutableArray new];
#if !TARGET_OS_OSX // [macOS]
    self.multipleTouchEnabled = YES;
#endif // [macOS]
    _useCustomContainerView = NO;
    _removeClippedSubviews = NO;
#if TARGET_OS_OSX // [macOS
    _allowsVibrancy = NO;
    self.mouseDownCanMoveWindow = YES;
#endif // macOS]
  }
  return self;
}

- (facebook::react::Props::Shared)props
{
  return _props;
}

#if !TARGET_OS_OSX // [macOS]
- (void)setContentView:(RCTUIView *)contentView // [macOS]
#else // [macOS
- (void)setContentView:(RCTPlatformView *)contentView // [macOS]
#endif // macOS]
{
  if (_contentView) {
    [_contentView removeFromSuperview];
  }

  _contentView = contentView;

  if (_contentView) {
    [self.currentContainerView addSubview:_contentView];
    _contentView.frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame());
#if TARGET_OS_OSX // [macOS
    _contentView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
#endif // macOS]
    [self addSubview:_contentView];
  }
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
  if (UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero)) {
    return [super pointInside:point withEvent:event];
  }
  CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets);
  return CGRectContainsPoint(hitFrame, point);
}

- (RCTUIColor *)backgroundColor // [macOS]
{
  return _backgroundColor;
}

- (void)setBackgroundColor:(RCTUIColor *)backgroundColor // [macOS]
{
  _backgroundColor = backgroundColor;
}

#if TARGET_OS_OSX // [macOS
- (void)resetCursorRects
{
  [self discardCursorRects];
  if (_props->cursor != Cursor::Auto)
  {
    NSCursor *cursor = NSCursorFromRCTCursor(RCTCursorFromCursor(_props->cursor));
    [self addCursorRect:self.bounds cursor:cursor];
  }
}

- (BOOL)allowsVibrancy
{
  return _allowsVibrancy;
}
#endif // macOS]

#if !TARGET_OS_OSX
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
  [super traitCollectionDidChange:previousTraitCollection];

  if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
    [self invalidateLayer];
  }
}
#else // [macOS
- (void)viewDidChangeEffectiveAppearance
{
  [super viewDidChangeEffectiveAppearance];

  [self invalidateLayer];
}
#endif // macOS]

#pragma mark - RCTComponentViewProtocol

+ (ComponentDescriptorProvider)componentDescriptorProvider
{
  RCTAssert(
      self == [RCTViewComponentView class],
      @"`+[RCTComponentViewProtocol componentDescriptorProvider]` must be implemented for all subclasses (and `%@` particularly).",
      NSStringFromClass([self class]));
  return concreteComponentDescriptorProvider<ViewComponentDescriptor>();
}

- (void)mountChildComponentView:(RCTUIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index // [macOS]
{
  RCTAssert(
      childComponentView.superview == nil,
      @"Attempt to mount already mounted component view. (parent: %@, child: %@, index: %@, existing parent: %@)",
      self,
      childComponentView,
      @(index),
      @([childComponentView.superview tag]));

  if (_removeClippedSubviews) {
    [_reactSubviews insertObject:childComponentView atIndex:index];
  } else {
    [self.currentContainerView insertSubview:childComponentView atIndex:index];
  }
}

- (void)unmountChildComponentView:(RCTUIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index // [macOS]
{
  if (_removeClippedSubviews) {
    [_reactSubviews removeObjectAtIndex:index];
  } else {
    RCTAssert(
        childComponentView.superview == self.currentContainerView,
        @"Attempt to unmount a view which is mounted inside different view. (parent: %@, child: %@, index: %@)",
        self,
        childComponentView,
        @(index));
    RCTAssert(
        (self.currentContainerView.subviews.count > index) &&
            [self.currentContainerView.subviews objectAtIndex:index] == childComponentView,
        @"Attempt to unmount a view which has a different index. (parent: %@, child: %@, index: %@, actual index: %@, tag at index: %@)",
        self,
        childComponentView,
        @(index),
        @([self.currentContainerView.subviews indexOfObject:childComponentView]),
        @([[self.currentContainerView.subviews objectAtIndex:index] tag]));
  }

  [childComponentView removeFromSuperview];
}

- (void)updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(RCTUIView *)clipView // [macOS]
{
  if (!_removeClippedSubviews) {
    // Use default behavior if unmounting is disabled
    return [super updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView];
  }

  if (_reactSubviews.count == 0) {
    // Do nothing if we have no subviews
    return;
  }

  if (CGSizeEqualToSize(self.bounds.size, CGSizeZero)) {
    // Do nothing if layout hasn't happened yet
    return;
  }

  // Convert clipping rect to local coordinates
  clipRect = [clipView convertRect:clipRect toView:self];

  // Mount / unmount views
  for (RCTUIView *view in _reactSubviews) { // [macOS]
    if (CGRectIntersectsRect(clipRect, view.frame)) {
      // View is at least partially visible, so remount it if unmounted
      [self.currentContainerView addSubview:view];
      // View is visible, update clipped subviews
      [view updateClippedSubviewsWithClipRect:clipRect relativeToView:self];
    } else if (view.superview) {
      // View is completely outside the clipRect, so unmount it
      [view removeFromSuperview];
    }
  }
}

- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
{
  RCTAssert(props, @"`props` must not be `null`.");

#ifndef NS_BLOCK_ASSERTIONS
  auto propsRawPtr = _props.get();
  RCTAssert(
      propsRawPtr &&
          ([self class] == [RCTViewComponentView class] ||
           typeid(*propsRawPtr).hash_code() != typeid(const ViewProps).hash_code()),
      @"`RCTViewComponentView` subclasses (and `%@` particularly) must setup `_props`"
       " instance variable with a default value in the constructor.",
      NSStringFromClass([self class]));
#endif

  const auto &oldViewProps = static_cast<const ViewProps &>(*_props);
  const auto &newViewProps = static_cast<const ViewProps &>(*props);

  BOOL needsInvalidateLayer = NO;

  // `opacity`
  if (oldViewProps.opacity != newViewProps.opacity &&
      ![_propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN containsObject:@"opacity"]) {
    self.layer.opacity = (float)newViewProps.opacity;
    needsInvalidateLayer = YES;
  }

  // Disable `removeClippedSubviews` when Fabric View Culling is enabled.
  if (!ReactNativeFeatureFlags::enableViewCulling()) {
    if (oldViewProps.removeClippedSubviews != newViewProps.removeClippedSubviews) {
      _removeClippedSubviews = newViewProps.removeClippedSubviews;
      if (_removeClippedSubviews && self.currentContainerView.subviews.count > 0) {
        _reactSubviews = [NSMutableArray arrayWithArray:self.currentContainerView.subviews];
      }
    }
  }

  // `backgroundColor`
  if (oldViewProps.backgroundColor != newViewProps.backgroundColor) {
    self.backgroundColor = RCTUIColorFromSharedColor(newViewProps.backgroundColor); // [macOS]
    needsInvalidateLayer = YES;
  }

  // `shadowColor`
  if (oldViewProps.shadowColor != newViewProps.shadowColor) {
    RCTUIColor *shadowColor = RCTUIColorFromSharedColor(newViewProps.shadowColor); // [macOS]
    self.layer.shadowColor = shadowColor.CGColor;
    needsInvalidateLayer = YES;
  }

  // `shadowOffset`
  if (oldViewProps.shadowOffset != newViewProps.shadowOffset) {
    self.layer.shadowOffset = RCTCGSizeFromSize(newViewProps.shadowOffset);
    needsInvalidateLayer = YES;
  }

  // `shadowOpacity`
  if (oldViewProps.shadowOpacity != newViewProps.shadowOpacity) {
    self.layer.shadowOpacity = (float)newViewProps.shadowOpacity;
    needsInvalidateLayer = YES;
  }

  // `shadowRadius`
  if (oldViewProps.shadowRadius != newViewProps.shadowRadius) {
    self.layer.shadowRadius = (CGFloat)newViewProps.shadowRadius;
    needsInvalidateLayer = YES;
  }

  // `backfaceVisibility`
  if (oldViewProps.backfaceVisibility != newViewProps.backfaceVisibility) {
    self.layer.doubleSided = newViewProps.backfaceVisibility == BackfaceVisibility::Visible;
  }

  // `cursor`
  if (oldViewProps.cursor != newViewProps.cursor) {
    needsInvalidateLayer = YES;
  }

  // `shouldRasterize`
  if (oldViewProps.shouldRasterize != newViewProps.shouldRasterize) {
    self.layer.shouldRasterize = newViewProps.shouldRasterize;
#if !TARGET_OS_OSX // [macOS]
    self.layer.rasterizationScale = newViewProps.shouldRasterize ? self.traitCollection.displayScale : 1.0;
#else // [macOS
    self.layer.rasterizationScale = 1.0;
#endif // macOS]
  }

  // `pointerEvents`
  if (oldViewProps.pointerEvents != newViewProps.pointerEvents) {
    self.userInteractionEnabled = newViewProps.pointerEvents != PointerEventsMode::None;
  }

  // `transform`
  if ((oldViewProps.transform != newViewProps.transform ||
       oldViewProps.transformOrigin != newViewProps.transformOrigin) &&
      ![_propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN containsObject:@"transform"]) {
    auto newTransform = newViewProps.resolveTransform(_layoutMetrics);
    CATransform3D caTransform = RCTCATransform3DFromTransformMatrix(newTransform);
#if !TARGET_OS_OSX // [macOS]
    self.layer.transform = caTransform;
#else // [macOS
    self.transform3D = caTransform;
#endif // macOS]

    // Enable edge antialiasing in rotation, skew, or perspective transforms
    self.layer.allowsEdgeAntialiasing = caTransform.m12 != 0.0f || caTransform.m21 != 0.0f || caTransform.m34 != 0.0f;
  }

  // `hitSlop`
  if (oldViewProps.hitSlop != newViewProps.hitSlop) {
    self.hitTestEdgeInsets = {
        -newViewProps.hitSlop.top,
        -newViewProps.hitSlop.left,
        -newViewProps.hitSlop.bottom,
        -newViewProps.hitSlop.right};
  }

  // `overflow`
  if (oldViewProps.getClipsContentToBounds() != newViewProps.getClipsContentToBounds()) {
    self.currentContainerView.clipsToBounds = newViewProps.getClipsContentToBounds();
    needsInvalidateLayer = YES;
  }

  // `border`
  if (oldViewProps.borderStyles != newViewProps.borderStyles || oldViewProps.borderRadii != newViewProps.borderRadii ||
      oldViewProps.borderColors != newViewProps.borderColors) {
    needsInvalidateLayer = YES;
  }

  // `outline`
  if (oldViewProps.outlineStyle != newViewProps.outlineStyle ||
      oldViewProps.outlineColor != newViewProps.outlineColor ||
      oldViewProps.outlineOffset != newViewProps.outlineOffset ||
      oldViewProps.outlineWidth != newViewProps.outlineWidth) {
    needsInvalidateLayer = YES;
  }

  // `nativeId`
  if (oldViewProps.nativeId != newViewProps.nativeId) {
    self.nativeId = RCTNSStringFromStringNilIfEmpty(newViewProps.nativeId);
  }

  // `accessible`
  if (oldViewProps.accessible != newViewProps.accessible) {
#if !TARGET_OS_OSX // [macOS]
    self.accessibilityElement.isAccessibilityElement = newViewProps.accessible;
#else // [macOS
    self.accessibilityElement.accessibilityElement = newViewProps.accessible;
#endif // macOS]
  }

  // `accessibilityLabel`
  if (oldViewProps.accessibilityLabel != newViewProps.accessibilityLabel) {
    self.accessibilityElement.accessibilityLabel = RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityLabel);
  }

#if !TARGET_OS_OSX // [macOS]
  // `accessibilityLanguage`
  if (oldViewProps.accessibilityLanguage != newViewProps.accessibilityLanguage) {
    self.accessibilityElement.accessibilityLanguage =
        RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityLanguage);
  }
#endif // [macOS]

  // `accessibilityHint`
  if (oldViewProps.accessibilityHint != newViewProps.accessibilityHint) {
#if !TARGET_OS_OSX // [macOS]
    self.accessibilityElement.accessibilityHint = RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityHint);
#else // [macOS
    self.accessibilityElement.accessibilityHelp = RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityHint);
#endif // macOS]
  }

#if !TARGET_OS_OSX // [macOS]
  // `accessibilityViewIsModal`
  if (oldViewProps.accessibilityViewIsModal != newViewProps.accessibilityViewIsModal) {
    self.accessibilityElement.accessibilityViewIsModal = newViewProps.accessibilityViewIsModal;
  }

  // `accessibilityElementsHidden`
  if (oldViewProps.accessibilityElementsHidden != newViewProps.accessibilityElementsHidden) {
    self.accessibilityElement.accessibilityElementsHidden = newViewProps.accessibilityElementsHidden;
  }

  // `accessibilityShowsLargeContentViewer`
  if (oldViewProps.accessibilityShowsLargeContentViewer != newViewProps.accessibilityShowsLargeContentViewer) {
    if (@available(iOS 13.0, *)) {
      if (newViewProps.accessibilityShowsLargeContentViewer) {
        self.showsLargeContentViewer = YES;
        UILargeContentViewerInteraction *interaction = [[UILargeContentViewerInteraction alloc] init];
        [self addInteraction:interaction];
      } else {
        self.showsLargeContentViewer = NO;
      }
    }
  }

  // `accessibilityLargeContentTitle`
  if (oldViewProps.accessibilityLargeContentTitle != newViewProps.accessibilityLargeContentTitle) {
    if (@available(iOS 13.0, *)) {
      self.largeContentTitle = RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityLargeContentTitle);
    }
  }
#endif // [macOS]

  // `accessibilityOrder`
  if (oldViewProps.accessibilityOrder != newViewProps.accessibilityOrder &&
      ReactNativeFeatureFlags::enableAccessibilityOrder()) {
    // Creating a set since a lot of logic requires lookups in here. However,
    // we still need to preserve the orginal order. So just read from props
    // if need to access that
    _accessibilityOrderNativeIDs = [NSMutableSet new];
    for (const std::string &childId : newViewProps.accessibilityOrder) {
      [_accessibilityOrderNativeIDs addObject:RCTNSStringFromString(childId)];
    }

    _accessibilityElements = [NSMutableArray new];
  }

  // `accessibilityTraits`
  if (oldViewProps.accessibilityTraits != newViewProps.accessibilityTraits) {
#if !TARGET_OS_OSX // [macOS]
    self.accessibilityElement.accessibilityTraits =
        RCTUIAccessibilityTraitsFromAccessibilityTraits(newViewProps.accessibilityTraits);
#else // [macOS
    // On macOS, accessibilityElement returns self (NSView*) which doesn't have accessibilityTraits.
    // Set role directly on self based on traits.
    RCTUIAccessibilityTraits traits = RCTUIAccessibilityTraitsFromAccessibilityTraits(newViewProps.accessibilityTraits);
    self.accessibilityRole = RCTAccessibilityRoleFromTraits(traits);
    self.accessibilityEnabled = (traits & RCTUIAccessibilityTraitNotEnabled) == 0;
#endif // macOS]
  }

#if !TARGET_OS_OSX // [macOS]
  // `accessibilityState`
  if (oldViewProps.accessibilityState != newViewProps.accessibilityState) {
    self.accessibilityTraits &= ~(UIAccessibilityTraitNotEnabled | UIAccessibilityTraitSelected);
    const auto accessibilityState = newViewProps.accessibilityState.value_or(AccessibilityState{});
    if (accessibilityState.selected) {
      self.accessibilityTraits |= UIAccessibilityTraitSelected;
    }
    if (accessibilityState.disabled) {
      self.accessibilityTraits |= UIAccessibilityTraitNotEnabled;
    }
  }

  // `accessibilityIgnoresInvertColors`
  if (oldViewProps.accessibilityIgnoresInvertColors != newViewProps.accessibilityIgnoresInvertColors) {
    self.accessibilityIgnoresInvertColors = newViewProps.accessibilityIgnoresInvertColors;
  }
#endif // [macOS]

  // `accessibilityValue`
  if (oldViewProps.accessibilityValue != newViewProps.accessibilityValue) {
    if (newViewProps.accessibilityValue.text.has_value()) {
      self.accessibilityElement.accessibilityValue =
          RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityValue.text.value());
    } else if (
        newViewProps.accessibilityValue.now.has_value() && newViewProps.accessibilityValue.min.has_value() &&
        newViewProps.accessibilityValue.max.has_value()) {
      CGFloat val = (CGFloat)(newViewProps.accessibilityValue.now.value()) /
          (newViewProps.accessibilityValue.max.value() - newViewProps.accessibilityValue.min.value());
      self.accessibilityElement.accessibilityValue =
          [NSNumberFormatter localizedStringFromNumber:@(val) numberStyle:NSNumberFormatterPercentStyle];
      ;
    } else {
      self.accessibilityElement.accessibilityValue = nil;
    }
  }

#if !TARGET_OS_OSX // [macOS]
  if (oldViewProps.accessibilityRespondsToUserInteraction != newViewProps.accessibilityRespondsToUserInteraction) {
    self.accessibilityElement.accessibilityRespondsToUserInteraction =
        newViewProps.accessibilityRespondsToUserInteraction;
  }
#endif // [macOS]

  // `testId`
  if (oldViewProps.testId != newViewProps.testId) {
    SEL setAccessibilityIdentifierSelector = @selector(setAccessibilityIdentifier:);
    NSString *identifier = RCTNSStringFromString(newViewProps.testId);
    if ([self.accessibilityElement respondsToSelector:setAccessibilityIdentifierSelector]) {
      RCTPlatformView *accessibilityView = (RCTPlatformView *)self.accessibilityElement; // [macOS]
      accessibilityView.accessibilityIdentifier = identifier;
    } else {
      self.accessibilityIdentifier = identifier;
    }
  }

  // `filter`
  if (oldViewProps.filter != newViewProps.filter) {
    needsInvalidateLayer = YES;
  }

  // `mixBlendMode`
  if (oldViewProps.mixBlendMode != newViewProps.mixBlendMode) {
    switch (newViewProps.mixBlendMode) {
      case BlendMode::Multiply:
        self.layer.compositingFilter = @"multiplyBlendMode";
        break;
      case BlendMode::Screen:
        self.layer.compositingFilter = @"screenBlendMode";
        break;
      case BlendMode::Overlay:
        self.layer.compositingFilter = @"overlayBlendMode";
        break;
      case BlendMode::Darken:
        self.layer.compositingFilter = @"darkenBlendMode";
        break;
      case BlendMode::Lighten:
        self.layer.compositingFilter = @"lightenBlendMode";
        break;
      case BlendMode::ColorDodge:
        self.layer.compositingFilter = @"colorDodgeBlendMode";
        break;
      case BlendMode::ColorBurn:
        self.layer.compositingFilter = @"colorBurnBlendMode";
        break;
      case BlendMode::HardLight:
        self.layer.compositingFilter = @"hardLightBlendMode";
        break;
      case BlendMode::SoftLight:
        self.layer.compositingFilter = @"softLightBlendMode";
        break;
      case BlendMode::Difference:
        self.layer.compositingFilter = @"differenceBlendMode";
        break;
      case BlendMode::Exclusion:
        self.layer.compositingFilter = @"exclusionBlendMode";
        break;
      case BlendMode::Hue:
        self.layer.compositingFilter = @"hueBlendMode";
        break;
      case BlendMode::Saturation:
        self.layer.compositingFilter = @"saturationBlendMode";
        break;
      case BlendMode::Color:
        self.layer.compositingFilter = @"colorBlendMode";
        break;
      case BlendMode::Luminosity:
        self.layer.compositingFilter = @"luminosityBlendMode";
        break;
      case BlendMode::Normal:
        self.layer.compositingFilter = nil;
        break;
    }
  }

  // `linearGradient`
  if (oldViewProps.backgroundImage != newViewProps.backgroundImage) {
    needsInvalidateLayer = YES;
  }

  // `boxShadow`
  if (oldViewProps.boxShadow != newViewProps.boxShadow) {
    needsInvalidateLayer = YES;
  }

#if TARGET_OS_OSX // [macOS
  // `acceptsFirstMouse`
  if (oldViewProps.acceptsFirstMouse != newViewProps.acceptsFirstMouse) {
    self.acceptsFirstMouse = newViewProps.acceptsFirstMouse;
  }

  // `mouseDownCanMoveWindow`
  if (oldViewProps.mouseDownCanMoveWindow != newViewProps.mouseDownCanMoveWindow) {
    self.mouseDownCanMoveWindow = newViewProps.mouseDownCanMoveWindow;
  }

  // `allowsVibrancy`
  if (oldViewProps.allowsVibrancy != newViewProps.allowsVibrancy) {
    _allowsVibrancy = newViewProps.allowsVibrancy;
  }

    // `draggedTypes`
  if (oldViewProps.draggedTypes != newViewProps.draggedTypes) {
    if (!oldViewProps.draggedTypes.empty()) {
      [self unregisterDraggedTypes];
    }

    if (!newViewProps.draggedTypes.empty()) {
      NSMutableArray<NSPasteboardType> *pasteboardTypes = [NSMutableArray arrayWithCapacity:newViewProps.draggedTypes.size()];
      for (const auto &draggedType : newViewProps.draggedTypes) {
        if (draggedType == "fileUrl") {
          [pasteboardTypes addObject:NSFilenamesPboardType];
        } else if (draggedType == "image") {
          [pasteboardTypes addObject:NSPasteboardTypePNG];
          [pasteboardTypes addObject:NSPasteboardTypeTIFF];
        } else if (draggedType == "string") {
          [pasteboardTypes addObject:NSPasteboardTypeString];
        }
      }
      [self registerForDraggedTypes:pasteboardTypes];
    }
  }

  // `tooltip`
  if (oldViewProps.tooltip != newViewProps.tooltip) {
    if (newViewProps.tooltip.has_value()) {
      self.toolTip = RCTNSStringFromStringNilIfEmpty(newViewProps.tooltip.value());
    } else {
      self.toolTip = nil;
    }
  }
#endif // macOS]

  _needsInvalidateLayer = _needsInvalidateLayer || needsInvalidateLayer;

  _props = std::static_pointer_cast<const ViewProps>(props);
}

- (void)updateEventEmitter:(const EventEmitter::Shared &)eventEmitter
{
  assert(std::dynamic_pointer_cast<const ViewEventEmitter>(eventEmitter));
  _eventEmitter = std::static_pointer_cast<const ViewEventEmitter>(eventEmitter);
}

- (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
           oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics
{
  // Using stored `_layoutMetrics` as `oldLayoutMetrics` here to avoid
  // re-applying individual sub-values which weren't changed.
  [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:_layoutMetrics];

  _layoutMetrics = layoutMetrics;
  _needsInvalidateLayer = YES;

  _borderLayer.frame = self.layer.bounds;

  if (_contentView) {
    _contentView.frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame());
  }

  if (_containerView) {
    _containerView.frame = CGRectMake(0, 0, self.layer.bounds.size.width, self.layer.bounds.size.height);
  }

  if (_backgroundColorLayer) {
    _backgroundColorLayer.frame = CGRectMake(0, 0, self.layer.bounds.size.width, self.layer.bounds.size.height);
  }

  if ((_props->transformOrigin.isSet() || _props->transform.operations.size() > 0) &&
      layoutMetrics.frame.size != oldLayoutMetrics.frame.size) {
    auto newTransform = _props->resolveTransform(layoutMetrics);
#if !TARGET_OS_OSX // [macOS]
    self.layer.transform = RCTCATransform3DFromTransformMatrix(newTransform);
#else // [macOS
    self.transform3D = RCTCATransform3DFromTransformMatrix(newTransform);
#endif // macOS]
  }
}

- (BOOL)isJSResponder
{
  return _isJSResponder;
}

- (void)setIsJSResponder:(BOOL)isJSResponder
{
  _isJSResponder = isJSResponder;
}

- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
{
  [super finalizeUpdates:updateMask];
  _useCustomContainerView = [self styleWouldClipOverflowInk];
  if (!_needsInvalidateLayer) {
    return;
  }

  _needsInvalidateLayer = NO;
  [self invalidateLayer];

#if TARGET_OS_OSX // [macOS
  [self updateTrackingAreas];
  [self updateClipViewBoundsObserverIfNeeded];
#endif // macOS]
}

- (void)prepareForRecycle
{
  [super prepareForRecycle];

  // If view was managed by animated, its props need to align with UIView's properties.
  const auto &props = static_cast<const ViewProps &>(*_props);
  if ([_propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN containsObject:@"transform"]) {
    self.layer.transform = RCTCATransform3DFromTransformMatrix(props.transform);
  }
  if ([_propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN containsObject:@"opacity"]) {
    self.layer.opacity = (float)props.opacity;
  }

  _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN = nil;
  _eventEmitter.reset();
  _isJSResponder = NO;
  _removeClippedSubviews = NO;
  _reactSubviews = [NSMutableArray new];
  _accessibilityElements = [NSMutableArray new];
#if TARGET_OS_OSX // [macOS
    _allowsVibrancy = NO;
    self.acceptsFirstMouse = NO;
    self.mouseDownCanMoveWindow = YES;
#endif // macOS]
}

- (void)setPropKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN:(NSSet<NSString *> *_Nullable)props
{
  _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN = props;
}

- (NSSet<NSString *> *_Nullable)propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN
{
  return _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN;
}

- (RCTPlatformView *)betterHitTest:(CGPoint)point withEvent:(UIEvent *)event // [macOS]
{
  // This is a classic textbook implementation of `hitTest:` with a couple of improvements:
  //   * It does not stop algorithm if some touch is outside the view
  //     which does not have `clipToBounds` enabled.
  //   * Taking `layer.zIndex` field into an account is not required because
  //     lists of `ShadowView`s are already sorted based on `zIndex` prop.

#if !TARGET_OS_OSX // [macOS]
  if (!self.userInteractionEnabled || self.hidden || self.alpha < 0.01) {
#else // [macOS
  if (!self.userInteractionEnabled || self.hidden || self.alphaValue < 0.01 ) {
#endif // macOS]
    return nil;
  }

  BOOL isPointInside = [self pointInside:point withEvent:event];

  BOOL clipsToBounds = false;

  clipsToBounds = clipsToBounds || _layoutMetrics.overflowInset == EdgeInsets{};

  if (clipsToBounds && !isPointInside) {
    return nil;
  }

  for (RCTPlatformView *subview in [self.subviews reverseObjectEnumerator]) { // [macOS]
    RCTPlatformView *hitView = RCTUIViewHitTestWithEvent(subview, point, self, event); // [macOS]
    if (hitView) {
      return hitView;
    }
  }

  return isPointInside ? self : nil;
}

- (RCTPlatformView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event // [macOS]
{
  switch (_props->pointerEvents) {
    case PointerEventsMode::Auto:
      return [self betterHitTest:point withEvent:event];
    case PointerEventsMode::None:
      return nil;
    case PointerEventsMode::BoxOnly:
      return [self pointInside:point withEvent:event] ? self : nil;
    case PointerEventsMode::BoxNone:
      RCTPlatformView *view = [self betterHitTest:point withEvent:event]; // [macOS]
      return view != self ? view : nil;
  }
}

static RCTCornerRadii RCTCornerRadiiFromBorderRadii(BorderRadii borderRadii)
{
  return RCTCornerRadii{
      .topLeftHorizontal = (CGFloat)borderRadii.topLeft.horizontal,
      .topLeftVertical = (CGFloat)borderRadii.topLeft.vertical,
      .topRightHorizontal = (CGFloat)borderRadii.topRight.horizontal,
      .topRightVertical = (CGFloat)borderRadii.topRight.vertical,
      .bottomLeftHorizontal = (CGFloat)borderRadii.bottomLeft.horizontal,
      .bottomLeftVertical = (CGFloat)borderRadii.bottomLeft.vertical,
      .bottomRightHorizontal = (CGFloat)borderRadii.bottomRight.horizontal,
      .bottomRightVertical = (CGFloat)borderRadii.bottomRight.vertical};
}

static RCTCornerRadii
RCTCreateOutlineCornerRadiiFromBorderRadii(const BorderRadii &borderRadii, CGFloat outlineWidth, CGFloat outlineOffset)
{
  return RCTCornerRadii{
      borderRadii.topLeft.horizontal != 0 ? borderRadii.topLeft.horizontal + outlineWidth + outlineOffset : 0,
      borderRadii.topLeft.vertical != 0 ? borderRadii.topLeft.vertical + outlineWidth + outlineOffset : 0,
      borderRadii.topRight.horizontal != 0 ? borderRadii.topRight.horizontal + outlineWidth + outlineOffset : 0,
      borderRadii.topRight.vertical != 0 ? borderRadii.topRight.vertical + outlineWidth + outlineOffset : 0,
      borderRadii.bottomLeft.horizontal != 0 ? borderRadii.bottomLeft.horizontal + outlineWidth + outlineOffset : 0,
      borderRadii.bottomLeft.vertical != 0 ? borderRadii.bottomLeft.vertical + outlineWidth + outlineOffset : 0,
      borderRadii.bottomRight.horizontal != 0 ? borderRadii.bottomRight.horizontal + outlineWidth + outlineOffset : 0,
      borderRadii.bottomRight.vertical != 0 ? borderRadii.bottomRight.vertical + outlineWidth + outlineOffset : 0};
}

// To be used for CSS properties like `border` and `outline`.
static void RCTAddContourEffectToLayer(
    CALayer *layer,
    const RCTCornerRadii &cornerRadii,
    const RCTBorderColors &contourColors,
    const UIEdgeInsets &contourInsets,
    const RCTBorderStyle &contourStyle)
{
  RCTUIImage *image = RCTGetBorderImage( // [macOS]
      contourStyle, layer.bounds.size, cornerRadii, contourInsets, contourColors, [RCTUIColor clearColor], NO); // [macOS]

  if (image == nil) {
    layer.contents = nil;
  } else {
    CGSize imageSize = image.size;
    UIEdgeInsets imageCapInsets = image.capInsets;
    CGRect contentsCenter = CGRect{
        CGPoint{imageCapInsets.left / imageSize.width, imageCapInsets.top / imageSize.height},
        CGSize{(CGFloat)1.0 / imageSize.width, (CGFloat)1.0 / imageSize.height}};
    layer.contents = (id)image.CGImage;
    layer.contentsScale = image.scale;

    BOOL isResizable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero);
    if (isResizable) {
      layer.contentsCenter = contentsCenter;
    } else {
      layer.contentsCenter = CGRect{CGPoint{0.0, 0.0}, CGSize{1.0, 1.0}};
    }
  }

  // If mutations are applied inside of Animation block, it may cause layer to be animated.
  // To stop that, imperatively remove all animations from layer.
  [layer removeAllAnimations];
}

static RCTBorderColors RCTCreateRCTBorderColorsFromBorderColors(BorderColors borderColors)
{
  return RCTBorderColors{
      .top = RCTUIColorFromSharedColor(borderColors.top),
      .left = RCTUIColorFromSharedColor(borderColors.left),
      .bottom = RCTUIColorFromSharedColor(borderColors.bottom),
      .right = RCTUIColorFromSharedColor(borderColors.right)};
}

static CALayerCornerCurve CornerCurveFromBorderCurve(BorderCurve borderCurve)
{
  // The constants are available only starting from iOS 13
  // CALayerCornerCurve is a typealias on NSString *
  switch (borderCurve) {
    case BorderCurve::Continuous:
      return @"continuous"; // kCACornerCurveContinuous;
    case BorderCurve::Circular:
      return @"circular"; // kCACornerCurveCircular;
  }
}

static RCTBorderStyle RCTBorderStyleFromBorderStyle(BorderStyle borderStyle)
{
  switch (borderStyle) {
    case BorderStyle::Solid:
      return RCTBorderStyleSolid;
    case BorderStyle::Dotted:
      return RCTBorderStyleDotted;
    case BorderStyle::Dashed:
      return RCTBorderStyleDashed;
  }
}

#if TARGET_OS_OSX // [macOS
static RCTCursor RCTCursorFromCursor(Cursor cursor)
{
  switch (cursor) {
    case Cursor::Auto:
      return RCTCursorAuto;
    case Cursor::Alias:
      return RCTCursorAlias;
    case Cursor::AllScroll:
      return RCTCursorAllScroll;
    case Cursor::Cell:
      return RCTCursorCell;
    case Cursor::ColResize:
      return RCTCursorColResize;
    case Cursor::ContextMenu:
      return RCTCursorContextMenu;
    case Cursor::Copy:
      return RCTCursorCopy;
    case Cursor::Crosshair:
      return RCTCursorCrosshair;
    case Cursor::Default:
      return RCTCursorDefault;
    case Cursor::EResize:
      return RCTCursorEResize;
    case Cursor::EWResize:
      return RCTCursorEWResize;
    case Cursor::Grab:
      return RCTCursorGrab;
    case Cursor::Grabbing:
      return RCTCursorGrabbing;
    case Cursor::Help:
      return RCTCursorHelp;
    case Cursor::Move:
      return RCTCursorMove;
    case Cursor::NEResize:
      return RCTCursorNEResize;
    case Cursor::NESWResize:
      return RCTCursorNESWResize;
    case Cursor::NResize:
      return RCTCursorNResize;
    case Cursor::NSResize:
      return RCTCursorNSResize;
    case Cursor::NWResize:
      return RCTCursorNWResize;
    case Cursor::NWSEResize:
      return RCTCursorNWSEResize;
    case Cursor::NoDrop:
      return RCTCursorNoDrop;
    case Cursor::None:
      return RCTCursorNone;
    case Cursor::NotAllowed:
      return RCTCursorNotAllowed;
    case Cursor::Pointer:
      return RCTCursorPointer;
    case Cursor::Progress:
      return RCTCursorProgress;
    case Cursor::RowResize:
      return RCTCursorRowResize;
    case Cursor::SResize:
      return RCTCursorSResize;
    case Cursor::SEResize:
      return RCTCursorSEResize;
    case Cursor::SWResize:
      return RCTCursorSWResize;
    case Cursor::Text:
      return RCTCursorText;
    case Cursor::Url:
      return RCTCursorUrl;
    case Cursor::VerticalText:
      return RCTCursorVerticalText;
    case Cursor::WResize:
      return RCTCursorWResize;
    case Cursor::Wait:
      return RCTCursorWait;
    case Cursor::ZoomIn:
      return RCTCursorZoomIn;
    case Cursor::ZoomOut:
      return RCTCursorZoomOut;
  }
}
#endif // macOS]

static RCTBorderStyle RCTBorderStyleFromOutlineStyle(OutlineStyle outlineStyle)
{
  switch (outlineStyle) {
    case OutlineStyle::Solid:
      return RCTBorderStyleSolid;
    case OutlineStyle::Dotted:
      return RCTBorderStyleDotted;
    case OutlineStyle::Dashed:
      return RCTBorderStyleDashed;
  }
}

- (BOOL)styleWouldClipOverflowInk
{
  const auto borderMetrics = _props->resolveBorderMetrics(_layoutMetrics);
  BOOL nonZeroBorderWidth = !(borderMetrics.borderWidths.isUniform() && borderMetrics.borderWidths.left == 0);
  BOOL clipToPaddingBox = ReactNativeFeatureFlags::enableIOSViewClipToPaddingBox();
  return _props->getClipsContentToBounds() &&
      ((!_props->boxShadow.empty() || (clipToPaddingBox && nonZeroBorderWidth)) || _props->outlineWidth != 0);
}

// This UIView is the UIView that holds all subviews. It is sometimes not self
// because we want to render "overflow ink" that extends beyond the bounds of
// the view and is not affected by clipping.
- (RCTUIView *)currentContainerView // [macOS]
{
  if (_useCustomContainerView) {
    if (!_containerView) {
      _containerView = [[RCTPlatformView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)]; // [macOS]
      for (RCTPlatformView *subview in self.subviews) { // [macOS]
        [_containerView addSubview:subview];
      }
      _containerView.clipsToBounds = self.clipsToBounds;
      self.clipsToBounds = NO;
      _containerView.layer.mask = self.layer.mask;
      self.layer.mask = nil;
      [self addSubview:_containerView];
    }

    return _containerView;
  } else {
    if (_containerView) {
      for (RCTPlatformView *subview in _containerView.subviews) { // [macOS]
        [self addSubview:subview];
      }
      self.clipsToBounds = _containerView.clipsToBounds;
      self.layer.mask = _containerView.layer.mask;
      [_containerView removeFromSuperview];
      _containerView = nil;
    }

    return self;
  }
}

- (void)invalidateLayer
{
  CALayer *layer = self.layer;

  if (CGSizeEqualToSize(layer.bounds.size, CGSizeZero)) {
    return;
  }

#if TARGET_OS_OSX // [macOS
  // On macOS, clipsToBounds doesn't automatically set layer.masksToBounds like iOS does.
  // When _useCustomContainerView is true (boxShadow + overflow:hidden), the container
  // view handles clipping children while the main layer stays unclipped for the shadow.
  // The container view's masksToBounds is set in currentContainerView getter.
  if (_useCustomContainerView) {
    layer.masksToBounds = NO;
  } else {
    layer.masksToBounds = _props->getClipsContentToBounds();
  }
#endif // macOS]

  const auto borderMetrics = _props->resolveBorderMetrics(_layoutMetrics);

  // Stage 1. Shadow Path
  BOOL const layerHasShadow = layer.shadowOpacity > 0 && CGColorGetAlpha(layer.shadowColor) > 0;
  if (layerHasShadow) {
    if (CGColorGetAlpha(_backgroundColor.CGColor) > 0.999) {
      // If view has a solid background color, calculate shadow path from border.
      const RCTCornerInsets cornerInsets =
          RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero);
      CGPathRef shadowPath = RCTPathCreateWithRoundedRect(self.bounds, cornerInsets, nil, NO);
      layer.shadowPath = shadowPath;
      CGPathRelease(shadowPath);
    } else {
      // Can't accurately calculate box shadow, so fall back to pixel-based shadow.
      layer.shadowPath = nil;
    }
  } else {
    layer.shadowPath = nil;
  }

#if !TARGET_OS_OSX // [visionOS]
  // Stage 1.5. Cursor / Hover Effects
  if (@available(iOS 17.0, *)) {
    UIHoverStyle *hoverStyle = nil;
    if (_props->cursor == Cursor::Pointer) {
      const RCTCornerInsets cornerInsets =
          RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero);
#if TARGET_OS_IOS
      // Due to an Apple bug, it seems on iOS, UIShapes made with `[UIShape shapeWithBezierPath:]`
      // evaluate their shape on the superviews' coordinate space. This leads to the hover shape
      // rendering incorrectly on iOS, iOS apps in compatibility mode on visionOS, but not on visionOS.
      // To work around this, for iOS, we can calculate the border path based on `view.frame` (the
      // superview's coordinate space) instead of view.bounds.
      CGPathRef borderPath = RCTPathCreateWithRoundedRect(self.frame, cornerInsets, NULL, NO);
#else // TARGET_OS_VISION
      CGPathRef borderPath = RCTPathCreateWithRoundedRect(self.bounds, cornerInsets, NULL, NO);
#endif
      UIBezierPath *bezierPath = [UIBezierPath bezierPathWithCGPath:borderPath];
      CGPathRelease(borderPath);
      UIShape *shape = [UIShape shapeWithBezierPath:bezierPath];

      hoverStyle = [UIHoverStyle styleWithEffect:[UIHoverAutomaticEffect effect] shape:shape];
    }
    [self setHoverStyle:hoverStyle];
  }
#endif // [visionOS]

#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 170000 /* __IPHONE_17_0 */
  // Stage 1.5. Cursor / Hover Effects
  if (@available(iOS 17.0, *)) {
    UIHoverStyle *hoverStyle = nil;
    if (_props->cursor == Cursor::Pointer) {
      const RCTCornerInsets cornerInsets =
          RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero);
#if TARGET_OS_IOS
      // Due to an Apple bug, it seems on iOS, UIShapes made with `[UIShape shapeWithBezierPath:]`
      // evaluate their shape on the superviews' coordinate space. This leads to the hover shape
      // rendering incorrectly on iOS, iOS apps in compatibility mode on visionOS, but not on visionOS.
      // To work around this, for iOS, we can calculate the border path based on `view.frame` (the
      // superview's coordinate space) instead of view.bounds.
      CGPathRef borderPath = RCTPathCreateWithRoundedRect(self.frame, cornerInsets, NULL, NO);
#else // TARGET_OS_VISION
      CGPathRef borderPath = RCTPathCreateWithRoundedRect(self.bounds, cornerInsets, NULL, NO);
#endif
      UIBezierPath *bezierPath = [UIBezierPath bezierPathWithCGPath:borderPath];
      CGPathRelease(borderPath);
      UIShape *shape = [UIShape shapeWithBezierPath:bezierPath];

      hoverStyle = [UIHoverStyle styleWithEffect:[UIHoverAutomaticEffect effect] shape:shape];
    }
    [self setHoverStyle:hoverStyle];
  }
#endif
  const bool useCoreAnimationBorderRendering =
      borderMetrics.borderColors.isUniform() && borderMetrics.borderWidths.isUniform() &&
      borderMetrics.borderStyles.isUniform() && borderMetrics.borderStyles.left == BorderStyle::Solid &&
      borderMetrics.borderRadii.isUniform() &&
      (
          // iOS draws borders in front of the content whereas CSS draws them behind
          // the content. For this reason, only use iOS border drawing when clipping
          // or when the border is hidden.
          borderMetrics.borderWidths.left == 0 || self.currentContainerView.clipsToBounds ||
          (colorComponentsFromColor(borderMetrics.borderColors.left).alpha == 0 &&
           (*borderMetrics.borderColors.left).getUIColor() != nullptr));

  // background color
#if !TARGET_OS_OSX // [macOS]
  RCTUIColor *backgroundColor = [_backgroundColor resolvedColorWithTraitCollection:self.traitCollection];
#else // [macOS
  RCTUIColor *backgroundColor = [_backgroundColor resolvedColorWithAppearance:self.effectiveAppearance];
#endif // macOS]
  // The reason we sometimes do not set self.layer's backgroundColor is because
  // we want to support non-uniform border radii, which apple does not natively
  // support. To get this behavior we need to create a CGPath in the shape that
  // we want. If we mask self.layer to this path, we would be clipping subviews
  // which we may not want to do. The generalized solution in this case is just
  // create a new layer
  if (useCoreAnimationBorderRendering) {
    [_backgroundColorLayer removeFromSuperlayer];
    _backgroundColorLayer = nil;
    layer.backgroundColor = backgroundColor.CGColor;
  } else {
    layer.backgroundColor = nil;
    if (!_backgroundColorLayer) {
      _backgroundColorLayer = [CALayer layer];
      _backgroundColorLayer.zPosition = BACKGROUND_COLOR_ZPOSITION;
      [self.layer addSublayer:_backgroundColorLayer];
    }
    [self shapeLayerToMatchView:_backgroundColorLayer borderMetrics:borderMetrics];
    _backgroundColorLayer.backgroundColor = backgroundColor.CGColor;
    [_backgroundColorLayer removeAllAnimations];
  }

  // borders
  if (useCoreAnimationBorderRendering) {
    [_borderLayer removeFromSuperlayer];
    _borderLayer = nil;

    layer.borderWidth = (CGFloat)borderMetrics.borderWidths.left;
    RCTUIColor *borderColor = RCTUIColorFromSharedColor(borderMetrics.borderColors.left); // [macOS]
#if TARGET_OS_OSX // [macOS
    borderColor = [borderColor resolvedColorWithAppearance:self.effectiveAppearance];
#endif // macOS]
    layer.borderColor = borderColor.CGColor;
    layer.cornerRadius = (CGFloat)borderMetrics.borderRadii.topLeft.horizontal;

    layer.cornerCurve = CornerCurveFromBorderCurve(borderMetrics.borderCurves.topLeft);

    layer.backgroundColor = backgroundColor.CGColor;
  } else {
    if (!_borderLayer) {
      CALayer *borderLayer = [CALayer new];
      borderLayer.zPosition = BACKGROUND_COLOR_ZPOSITION + 1;
      borderLayer.frame = layer.bounds;
      borderLayer.magnificationFilter = kCAFilterNearest;
      [layer addSublayer:borderLayer];
      _borderLayer = borderLayer;
    }

    layer.borderWidth = 0;
    layer.borderColor = nil;
    layer.cornerRadius = 0;

    RCTBorderColors borderColors = RCTCreateRCTBorderColorsFromBorderColors(borderMetrics.borderColors);
#if TARGET_OS_OSX // [macOS
    borderColors.top = [borderColors.top resolvedColorWithAppearance:self.effectiveAppearance];
    borderColors.left = [borderColors.left resolvedColorWithAppearance:self.effectiveAppearance];
    borderColors.bottom = [borderColors.bottom resolvedColorWithAppearance:self.effectiveAppearance];
    borderColors.right = [borderColors.right resolvedColorWithAppearance:self.effectiveAppearance];
#endif // macOS]

    RCTAddContourEffectToLayer(
        _borderLayer,
        RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii),
        borderColors,
        RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths),
        RCTBorderStyleFromBorderStyle(borderMetrics.borderStyles.left));
  }

  // outline
  [_outlineLayer removeFromSuperlayer];
  _outlineLayer = nil;
  if (_props->outlineWidth != 0) {
    if (!_outlineLayer) {
      CALayer *outlineLayer = [CALayer new];
      outlineLayer.magnificationFilter = kCAFilterNearest;
      outlineLayer.zPosition = BACKGROUND_COLOR_ZPOSITION + 2;

      [layer addSublayer:outlineLayer];
      _outlineLayer = outlineLayer;
    }
    _outlineLayer.frame = CGRectInset(
        layer.bounds, -_props->outlineOffset - _props->outlineWidth, -_props->outlineOffset - _props->outlineWidth);

    if (borderMetrics.borderRadii.isUniform() && borderMetrics.borderRadii.topLeft.horizontal == 0) {
      RCTUIColor *outlineColor = RCTUIColorFromSharedColor(_props->outlineColor); // [macOS]
#if TARGET_OS_OSX // [macOS
      outlineColor = [outlineColor resolvedColorWithAppearance:self.effectiveAppearance];
#endif // macOS]
      _outlineLayer.borderWidth = _props->outlineWidth;
      _outlineLayer.borderColor = outlineColor.CGColor;
    } else {
      RCTUIColor *outlineColor = RCTUIColorFromSharedColor(_props->outlineColor); // [macOS]
#if TARGET_OS_OSX // [macOS
      outlineColor = [outlineColor resolvedColorWithAppearance:self.effectiveAppearance];
#endif // macOS]

      RCTAddContourEffectToLayer(
          _outlineLayer,
          RCTCreateOutlineCornerRadiiFromBorderRadii(
              borderMetrics.borderRadii, _props->outlineWidth, _props->outlineOffset),
          RCTBorderColors{outlineColor, outlineColor, outlineColor, outlineColor},
          UIEdgeInsets{_props->outlineWidth, _props->outlineWidth, _props->outlineWidth, _props->outlineWidth},
          RCTBorderStyleFromOutlineStyle(_props->outlineStyle));
    }
  }

  // filter
  [_filterLayer removeFromSuperlayer];
  _filterLayer = nil;
  self.layer.opacity = (float)_props->opacity;
  if (!_props->filter.empty()) {
    float multiplicativeBrightness = 1;
    for (const auto &primitive : _props->filter) {
      if (std::holds_alternative<Float>(primitive.parameters)) {
        if (primitive.type == FilterType::Brightness) {
          multiplicativeBrightness *= std::get<Float>(primitive.parameters);
        } else if (primitive.type == FilterType::Opacity) {
          self.layer.opacity *= std::get<Float>(primitive.parameters);
        }
      }
    }

    _filterLayer = [CALayer layer];
    [self shapeLayerToMatchView:_filterLayer borderMetrics:borderMetrics];
    _filterLayer.compositingFilter = @"multiplyBlendMode";
    _filterLayer.backgroundColor = [RCTUIColor colorWithRed:multiplicativeBrightness // [macOS]
                                                      green:multiplicativeBrightness
                                                       blue:multiplicativeBrightness
                                                      alpha:self.layer.opacity]
                                       .CGColor;
    // So that this layer is always above any potential sublayers this view may
    // add
    _filterLayer.zPosition = CGFLOAT_MAX;
    [self.layer addSublayer:_filterLayer];
  }

  // background image
  [self clearExistingBackgroundImageLayers];
  if (!_props->backgroundImage.empty()) {
    // iterate in reverse to match CSS specification
    for (const auto &backgroundImage : std::ranges::reverse_view(_props->backgroundImage)) {
      if (std::holds_alternative<LinearGradient>(backgroundImage)) {
        const auto &linearGradient = std::get<LinearGradient>(backgroundImage);
        CALayer *backgroundImageLayer = [RCTLinearGradient gradientLayerWithSize:self.layer.bounds.size
                                                                        gradient:linearGradient];
        [self shapeLayerToMatchView:backgroundImageLayer borderMetrics:borderMetrics];
        backgroundImageLayer.masksToBounds = YES;
        backgroundImageLayer.zPosition = BACKGROUND_COLOR_ZPOSITION;
        [self.layer addSublayer:backgroundImageLayer];
        [_backgroundImageLayers addObject:backgroundImageLayer];
      } else if (std::holds_alternative<RadialGradient>(backgroundImage)) {
        const auto &radialGradient = std::get<RadialGradient>(backgroundImage);
        CALayer *backgroundImageLayer = [RCTRadialGradient gradientLayerWithSize:self.layer.bounds.size
                                                                        gradient:radialGradient];
        [self shapeLayerToMatchView:backgroundImageLayer borderMetrics:borderMetrics];
        backgroundImageLayer.masksToBounds = YES;
        backgroundImageLayer.zPosition = BACKGROUND_COLOR_ZPOSITION;
        [self.layer addSublayer:backgroundImageLayer];
        [_backgroundImageLayers addObject:backgroundImageLayer];
      }
    }
  }

  // box shadow
  for (CALayer *boxShadowLayer in _boxShadowLayers) {
    [boxShadowLayer removeFromSuperlayer];
  }
  [_boxShadowLayers removeAllObjects];
  if (!_props->boxShadow.empty()) {
    if (!_boxShadowLayers) {
      _boxShadowLayers = [NSMutableArray new];
    }
    for (auto it = _props->boxShadow.rbegin(); it != _props->boxShadow.rend(); ++it) {
      CALayer *shadowLayer = RCTGetBoxShadowLayer(
          *it,
          RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii),
          RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths),
          self.layer.bounds.size);
      shadowLayer.zPosition = _borderLayer.zPosition;
      [self.layer addSublayer:shadowLayer];
      [_boxShadowLayers addObject:shadowLayer];
    }
  }

  // clipping
  self.currentContainerView.layer.mask = nil;
  if (self.currentContainerView.clipsToBounds) {
    BOOL clipToPaddingBox = ReactNativeFeatureFlags::enableIOSViewClipToPaddingBox();
    if (!clipToPaddingBox) {
      if (borderMetrics.borderRadii.isUniform()) {
        self.currentContainerView.layer.cornerRadius = borderMetrics.borderRadii.topLeft.horizontal;
      } else {
        CALayer *maskLayer =
            [self createMaskLayer:self.bounds
                     cornerInsets:RCTGetCornerInsets(
                                      RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero)];
        self.currentContainerView.layer.mask = maskLayer;
      }

      for (RCTPlatformView *subview in self.currentContainerView.subviews) { // [macOS]
        if ([subview isKindOfClass:[RCTUIImageView class]]) { // [macOS]
          RCTCornerInsets cornerInsets = RCTGetCornerInsets(
              RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii),
              RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths));

          // If the subview is an image view, we have to apply the mask directly to the image view's layer,
          // otherwise the image might overflow with the border radius.
          subview.layer.mask = [self createMaskLayer:subview.bounds cornerInsets:cornerInsets];
        }
      }
    } else if (
        !borderMetrics.borderWidths.isUniform() || borderMetrics.borderWidths.left != 0 ||
        !borderMetrics.borderRadii.isUniform()) {
      CALayer *maskLayer = [self createMaskLayer:RCTCGRectFromRect(_layoutMetrics.getPaddingFrame())
                                    cornerInsets:RCTGetCornerInsets(
                                                     RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii),
                                                     RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths))];
      self.currentContainerView.layer.mask = maskLayer;
    } else {
      self.currentContainerView.layer.cornerRadius = borderMetrics.borderRadii.topLeft.horizontal;
    }
  }
}

// Shapes the given layer to match the shape of this View's layer. This is
// basically just accounting for size, position, and border radius.
- (void)shapeLayerToMatchView:(CALayer *)layer borderMetrics:(BorderMetrics)borderMetrics
{
  // Bounds is needed here to account for scaling transforms properly and ensure
  // we do not scale twice
  layer.frame = CGRectMake(0, 0, self.layer.bounds.size.width, self.layer.bounds.size.height);
  if (borderMetrics.borderRadii.isUniform()) {
    layer.mask = nil;
    layer.cornerRadius = borderMetrics.borderRadii.topLeft.horizontal;
    layer.cornerCurve = CornerCurveFromBorderCurve(borderMetrics.borderCurves.topLeft);
  } else {
    CAShapeLayer *maskLayer = [self
        createMaskLayer:self.bounds
           cornerInsets:RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero)];
    layer.mask = maskLayer;
    layer.cornerRadius = 0;
  }
}

- (CAShapeLayer *)createMaskLayer:(CGRect)bounds cornerInsets:(RCTCornerInsets)cornerInsets
{
  CGPathRef path = RCTPathCreateWithRoundedRect(bounds, cornerInsets, nil, NO);
  CAShapeLayer *maskLayer = [CAShapeLayer layer];
  maskLayer.path = path;
  CGPathRelease(path);
  return maskLayer;
}

- (void)clearExistingBackgroundImageLayers
{
  if (_backgroundImageLayers == nil) {
    _backgroundImageLayers = [NSMutableArray new];
    return;
  }
  for (CALayer *backgroundImageLayer in _backgroundImageLayers) {
    [backgroundImageLayer removeFromSuperlayer];
  }
  [_backgroundImageLayers removeAllObjects];
}

#pragma mark - Accessibility

#if !TARGET_OS_OSX // [macOS]
- (NSObject *)accessibilityElement
#else // [macOS
- (NSView *)accessibilityElement // macOS]
#endif // macOS]
{
  return self;
}

- (NSArray<NSObject *> *)accessibilityElements
{
  if ([_accessibilityOrderNativeIDs count] <= 0) {
    return super.accessibilityElements;
  }

  // TODO: Currently this ignores changes to descendant nativeID's. While that should rarely, if ever happen, it's an
  // edge case we should address. Currently this fixes some app deaths so landing this without addressing that edge case
  // for now.
  if ([_accessibilityElements count] > 0) {
    return _accessibilityElements;
  }

  NSMutableDictionary<NSString *, RCTPlatformView *> *nativeIdToView = [NSMutableDictionary new]; // [macOS]

  [RCTViewComponentView collectAccessibilityElements:self
                                      intoDictionary:nativeIdToView
                                           nativeIds:_accessibilityOrderNativeIDs];

  for (auto childId : _props->accessibilityOrder) {
    NSString *nsStringChildId = RCTNSStringFromString(childId);
    // Special case to allow for self-referencing with accessibilityOrder
    if ([nsStringChildId isEqualToString:self.nativeId]) {
      if (!_axElementDescribingSelf) {
        _axElementDescribingSelf = [[RCTViewAccessibilityElement alloc] initWithView:self];
      }
      _axElementDescribingSelf.isAccessibilityElement = [super isAccessibilityElement];
      [_accessibilityElements addObject:_axElementDescribingSelf];
    } else {
      RCTPlatformView *viewWithMatchingNativeId = [nativeIdToView objectForKey:nsStringChildId]; // [macOS]
      if (viewWithMatchingNativeId) {
        [_accessibilityElements addObject:viewWithMatchingNativeId];
      }
    }
  }

  return _accessibilityElements;
}

+ (void)collectAccessibilityElements:(RCTPlatformView *)view // [macOS]
                      intoDictionary:(NSMutableDictionary<NSString *, RCTPlatformView *> *)dict // [macOS]
                           nativeIds:(NSSet<NSString *> *)nativeIds
{
  for (RCTPlatformView *subview in view.subviews) { // [macOS]
    if ([subview isKindOfClass:[RCTViewComponentView class]] &&
        [nativeIds containsObject:((RCTViewComponentView *)subview).nativeId]) {
      [dict setObject:subview forKey:((RCTViewComponentView *)subview).nativeId];
    }
    [RCTViewComponentView collectAccessibilityElements:subview intoDictionary:dict nativeIds:nativeIds];
  }
}

static NSString *RCTRecursiveAccessibilityLabel(RCTUIView *view) // [macOS]
{
  // Result string is initialized lazily to prevent useless but costly allocations.
  NSMutableString *result = nil;
  for (RCTUIView *subview in view.subviews) { // [macOS]
    NSString *label = subview.accessibilityLabel;
    if (!label) {
      label = RCTRecursiveAccessibilityLabel(subview);
    }
    if (label && label.length > 0) {
      if (result == nil) {
        result = [NSMutableString string];
      }
      if (result.length > 0) {
        [result appendString:@", "];
      }
      [result appendString:label];
    }
  }
  return result;
}

- (NSString *)accessibilityLabel
{
  NSString *label = super.accessibilityLabel;
  if (label) {
    return label;
  }

  if (self.isAccessibilityElement) {
    return RCTRecursiveAccessibilityLabel(self.currentContainerView);
  }
  return nil;
}

- (NSString *)accessibilityLabelForCoopting
{
  return super.accessibilityLabel;
}

- (BOOL)wantsToCooptLabel
{
  return !super.accessibilityLabel && super.isAccessibilityElement;
}

- (BOOL)isAccessibilityElement
{
  if (self.contentView != nil) {
    return self.contentView.isAccessibilityElement;
  }

  // If we reference ourselves in accessibilityOrder then we will make a
  // UIAccessibilityElement object to represent ourselves since returning YES
  // here would mean iOS would not call into accessibilityElements
  if ([_accessibilityOrderNativeIDs containsObject:self.nativeId]) {
    return NO;
  }

  return [super isAccessibilityElement];
}

- (NSString *)accessibilityValue
{
  const auto &props = static_cast<const ViewProps &>(*_props);
  const auto accessibilityState = props.accessibilityState.value_or(AccessibilityState{});

  // Handle Switch.
#if !TARGET_OS_OSX // [macOS]
  if ((self.accessibilityTraits & AccessibilityTraitSwitch) == AccessibilityTraitSwitch) {
    if (accessibilityState.checked == AccessibilityState::Checked) {
      return @"1";
    } else if (accessibilityState.checked == AccessibilityState::Unchecked) {
      return @"0";
    }
  }
#endif // [macOS]

  NSMutableArray *valueComponents = [NSMutableArray new];
  NSString *roleString = (props.role != Role::None) ? [NSString stringWithUTF8String:toString(props.role).c_str()]
                                                    : [NSString stringWithUTF8String:props.accessibilityRole.c_str()];

  // In iOS, checkbox and radio buttons aren't recognized as traits. However,
  // because our apps use checkbox and radio buttons often, we should announce
  // these to screenreader users.  (They should already be familiar with them
  // from using web).
  if ([roleString isEqualToString:@"checkbox"]) {
    [valueComponents addObject:RCTLocalizedString("checkbox", "checkable interactive control")];
  }

  if ([roleString isEqualToString:@"radio"]) {
    [valueComponents
        addObject:
            RCTLocalizedString(
                "radio button",
                "a checkable input that when associated with other radio buttons, only one of which can be checked at a time")];
  }

  // Handle states which haven't already been handled.
  if (accessibilityState.checked == AccessibilityState::Checked) {
    [valueComponents
        addObject:RCTLocalizedString("checked", "a checkbox, radio button, or other widget which is checked")];
  }
  if (accessibilityState.checked == AccessibilityState::Unchecked) {
    [valueComponents
        addObject:RCTLocalizedString("unchecked", "a checkbox, radio button, or other widget which is unchecked")];
  }
  if (accessibilityState.checked == AccessibilityState::Mixed) {
    [valueComponents
        addObject:RCTLocalizedString(
                      "mixed", "a checkbox, radio button, or other widget which is both checked and unchecked")];
  }
  if (accessibilityState.expanded.value_or(false)) {
    [valueComponents
        addObject:RCTLocalizedString("expanded", "a menu, dialog, accordian panel, or other widget which is expanded")];
  }

  if (accessibilityState.busy) {
    [valueComponents addObject:RCTLocalizedString("busy", "an element currently being updated or modified")];
  }

  // Using super.accessibilityValue:
  // 1. to access the value that is set to accessibilityValue in updateProps
  // 2. can't access from self.accessibilityElement because it resolves to self
  if (super.accessibilityValue) {
    [valueComponents addObject:super.accessibilityValue];
  }

  if (valueComponents.count > 0) {
    return [valueComponents componentsJoinedByString:@", "];
  }

  return nil;
}

#pragma mark - Accessibility Events

- (BOOL)shouldGroupAccessibilityChildren
{
  return YES;
}

- (NSArray<UIAccessibilityCustomAction *> *)accessibilityCustomActions
{
  const auto &accessibilityActions = _props->accessibilityActions;

  if (accessibilityActions.empty()) {
    return nil;
  }

  NSMutableArray<UIAccessibilityCustomAction *> *customActions = [NSMutableArray array];
  for (const auto &accessibilityAction : accessibilityActions) {
    [customActions
        addObject:[[UIAccessibilityCustomAction alloc] initWithName:RCTNSStringFromString(accessibilityAction.name)
                                                             target:self
                                                           selector:@selector(didActivateAccessibilityCustomAction:)]];
  }

  return [customActions copy];
}

- (BOOL)accessibilityActivate
{
  if (_eventEmitter && _props->onAccessibilityTap) {
    _eventEmitter->onAccessibilityTap();
    return YES;
  } else {
    return NO;
  }
}

- (BOOL)accessibilityPerformMagicTap
{
  if (_eventEmitter && _props->onAccessibilityMagicTap) {
    _eventEmitter->onAccessibilityMagicTap();
    return YES;
  } else {
    return NO;
  }
}

- (BOOL)accessibilityPerformEscape
{
  if (_eventEmitter && _props->onAccessibilityEscape) {
    _eventEmitter->onAccessibilityEscape();
    return YES;
  } else {
    return NO;
  }
}

- (void)accessibilityIncrement
{
  if (_eventEmitter && _props->onAccessibilityAction) {
    _eventEmitter->onAccessibilityAction("increment");
  }
}

- (void)accessibilityDecrement
{
  if (_eventEmitter && _props->onAccessibilityAction) {
    _eventEmitter->onAccessibilityAction("decrement");
  }
}

- (BOOL)didActivateAccessibilityCustomAction:(UIAccessibilityCustomAction *)action
{
  if (_eventEmitter && _props->onAccessibilityAction) {
    _eventEmitter->onAccessibilityAction(RCTStringFromNSString(action.name));
    return YES;
  } else {
    return NO;
  }
}

#if TARGET_OS_OSX // [macOS
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
{
  if ([commandName isEqualToString:@"focus"]) {
    [self focus];
    return;
  }

  if ([commandName isEqualToString:@"blur"]) {
    [self blur];
    return;
  }
}

# pragma mark - Focus Ring

- (CGRect)focusRingMaskBounds
{
  return [self bounds];
}

- (void)drawFocusRingMask
{
  if (_props->enableFocusRing) {
    CGContextRef context = NSGraphicsContext.currentContext.CGContext;

    const auto borderMetrics = _props->resolveBorderMetrics(_layoutMetrics);
    const RCTCornerInsets cornerInsets =
        RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero);
    CGPathRef path = RCTPathCreateWithRoundedRect(self.bounds, cornerInsets, NULL, NO); // [macOS]

    CGContextAddPath(context, path);
    CGContextFillPath(context);
    CGPathRelease(path);
  }
}


#pragma mark - Focus Events

- (void)focus
{
  [[self window] makeFirstResponder:self];
}

- (void)blur
{
  [[self window] resignFirstResponder];
}

- (BOOL)needsPanelToBecomeKey
{
	// We need to override this so that mouse clicks don't move keyboard focus on focusable views by default.
	return false;
}

- (BOOL)acceptsFirstResponder
{
  return _props->focusable || [super acceptsFirstResponder];
}

- (BOOL)becomeFirstResponder
{
  if (![super becomeFirstResponder]) {
    return NO;
  }

  if (_eventEmitter) {
    _eventEmitter->onFocus();
  }

  return YES;
}

-  (BOOL)resignFirstResponder
{
  if (![super resignFirstResponder]) {
    return NO;
  }

  if (_eventEmitter) {
    _eventEmitter->onBlur();
  }

  return YES;
}

#pragma mark - Keyboard Events

- (BOOL)handleKeyboardEvent:(NSEvent *)event {
  RCTAssert(
    event.type == NSEventTypeKeyDown || event.type == NSEventTypeKeyUp,
    @"Keyboard event must be keyDown, keyUp. Got type: %ld", (long)event.type);

  // Convert the event to a KeyEvent
  NSEventModifierFlags modifierFlags = event.modifierFlags;
  facebook::react::KeyEvent keyEvent = {
    .key = [[RCTViewKeyboardEvent keyFromEvent:event] UTF8String],
    .altKey = static_cast<bool>(modifierFlags & NSEventModifierFlagOption),
    .ctrlKey = static_cast<bool>(modifierFlags & NSEventModifierFlagControl),
    .shiftKey = static_cast<bool>(modifierFlags & NSEventModifierFlagShift),
    .metaKey = static_cast<bool>(modifierFlags & NSEventModifierFlagCommand),
    .capsLockKey = static_cast<bool>(modifierFlags & NSEventModifierFlagCapsLock),
    .numericPadKey = static_cast<bool>(modifierFlags & NSEventModifierFlagNumericPad),
    .helpKey = static_cast<bool>(modifierFlags & NSEventModifierFlagHelp),
    .functionKey = static_cast<bool>(modifierFlags & NSEventModifierFlagFunction),
  };

  if (_eventEmitter) {
    // Emit the event to JS only once. By default, events, will bubble up the respnder chain
    // when we call super, so let's emit the event only at the first responder. It would be
    // simpler to check `if (self == self.window.firstResponder), however, that does not account
    // for cases like TextInputComponentView, where the first responder may be a subview.
    static const char kRCTViewKeyboardEventEmittedKey = 0;
    NSNumber *emitted = objc_getAssociatedObject(event, &kRCTViewKeyboardEventEmittedKey);
    BOOL alreadyEmitted = [emitted boolValue];
    if (!alreadyEmitted) {
      if (event.type == NSEventTypeKeyDown) {
        _eventEmitter->onKeyDown(keyEvent);
      } else {
        _eventEmitter->onKeyUp(keyEvent);
      }
      objc_setAssociatedObject(event, &kRCTViewKeyboardEventEmittedKey, @(YES), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
  }

  // If keyDownEvents or keyUpEvents specifies the event, block native handling of the event
  auto const& handledKeyEvents = event.type == NSEventTypeKeyDown ? _props->keyDownEvents : _props->keyUpEvents;
  return std::find(handledKeyEvents.cbegin(), handledKeyEvents.cend(), keyEvent) != handledKeyEvents.cend();
}

- (void)keyDown:(NSEvent *)event {
  if (![self handleKeyboardEvent:event]) {
    [super keyDown:event];
  }
}

- (void)keyUp:(NSEvent *)event {
  if (![self handleKeyboardEvent:event]) {
    [super keyUp:event];
  }
}


#pragma mark - Drag and Drop Events

enum DragEventType {
  DragEnter,
  DragLeave,
  Drop,
};

- (DataTransfer)dataTransferForPasteboard:(NSPasteboard *)pasteboard {
  DataTransfer dataTransfer{};
  auto &files = dataTransfer.files;
  auto &items = dataTransfer.items;
  auto &types = dataTransfer.types;

  NSArray *fileNames = [pasteboard propertyListForType:NSFilenamesPboardType] ?: @[];
  for (NSString *file in fileNames) {
    NSURL *fileURL = [NSURL fileURLWithPath:file];
    BOOL isDir = NO;
    BOOL isValid = [[NSFileManager defaultManager] fileExistsAtPath:fileURL.path isDirectory:&isDir] && !isDir;
    if (isValid) {

      NSString *MIMETypeString = nil;
      if (fileURL.pathExtension) {
        CFStringRef fileExtension = (__bridge CFStringRef)fileURL.pathExtension;
        CFStringRef UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileExtension, NULL);
        if (UTI != NULL) {
          CFStringRef MIMEType = UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType);
          CFRelease(UTI);
          MIMETypeString = (__bridge_transfer NSString *)MIMEType;
        }
      }

      NSNumber *fileSizeValue = nil;
      NSError *fileSizeError = nil;
      BOOL success = [fileURL getResourceValue:&fileSizeValue
                                        forKey:NSURLFileSizeKey
                                         error:&fileSizeError];

      std::string typeString = MIMETypeString != nil ? [MIMETypeString UTF8String] : "";

      DataTransferFile fileEntry = {
        .name = fileURL.lastPathComponent ? fileURL.lastPathComponent.UTF8String : "",
        .type = typeString,
        .uri = fileURL.path ? fileURL.path.UTF8String : "",
      };

      if (success) {
        fileEntry.size = fileSizeValue.intValue;
      }

      if ([MIMETypeString hasPrefix:@"image/"]) {
        NSImage *image = [[NSImage alloc] initWithContentsOfURL:fileURL];
        CGImageRef cgImage = [image CGImageForProposedRect:nil context:nil hints:nil];
        fileEntry.width = static_cast<int>(CGImageGetWidth(cgImage));
        fileEntry.height = static_cast<int>(CGImageGetHeight(cgImage));
      }

      files.push_back(fileEntry);
      items.push_back({
          .kind = "file",
          .type = typeString,
      });
      types.push_back(typeString);
    }
  }

  NSPasteboardType imageType = [pasteboard availableTypeFromArray:@[NSPasteboardTypePNG, NSPasteboardTypeTIFF]];
  if (imageType && fileNames.count == 0) {
    NSString *MIMETypeString = imageType == NSPasteboardTypePNG ?[UTTypePNG preferredMIMEType] : [UTTypeTIFF preferredMIMEType];
    NSData *imageData = [pasteboard dataForType:imageType];
    NSImage *image = [[NSImage alloc] initWithData:imageData];
    CGImageRef cgImage = [image CGImageForProposedRect:nil context:nil hints:nil];

    NSString *dataURLString = RCTDataURL(MIMETypeString, imageData).absoluteString;
    std::string typeString = MIMETypeString != nil ? [MIMETypeString UTF8String] : "";

    DataTransferFile fileEntry = {
      .name = "",
      .type = typeString,
      .uri = dataURLString ? dataURLString.UTF8String : "",
    };

    fileEntry.size = static_cast<int>(imageData.length);
    fileEntry.width = static_cast<int>(CGImageGetWidth(cgImage));
    fileEntry.height = static_cast<int>(CGImageGetHeight(cgImage));

    files.push_back(fileEntry);
    items.push_back({
        .kind = "image",
        .type = typeString,
    });
    types.push_back(typeString);
  }
  return dataTransfer;
}

- (void)emitDragEvent:(DragEventType)eventType draggingInfo:(id<NSDraggingInfo>)sender {
  if (!_eventEmitter) {
    return;
  }

  NSPoint locationInWindow = sender.draggingLocation;
  NSPasteboard *pasteboard = sender.draggingPasteboard;

  DataTransfer dataTransfer = [self dataTransferForPasteboard:pasteboard];

  NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil];
  NSEventModifierFlags modifierFlags = self.window.currentEvent.modifierFlags;

  DragEvent dragEvent = {
    {
      .clientX = locationInView.x,
      .clientY = locationInView.y,
      .screenX = locationInWindow.x,
      .screenY = locationInWindow.y,
      .altKey = static_cast<bool>(modifierFlags & NSEventModifierFlagOption),
      .ctrlKey = static_cast<bool>(modifierFlags & NSEventModifierFlagControl),
      .shiftKey = static_cast<bool>(modifierFlags & NSEventModifierFlagShift),
      .metaKey = static_cast<bool>(modifierFlags & NSEventModifierFlagCommand),
    },
    .dataTransfer = dataTransfer,
  };

  switch (eventType) {
    case DragEnter:
      _eventEmitter->onDragEnter(dragEvent);
      break;

    case DragLeave:
      _eventEmitter->onDragLeave(dragEvent);
      break;

    case Drop:
      _eventEmitter->onDrop(dragEvent);
      break;
  }
}

- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
{
  NSPasteboard *pboard = sender.draggingPasteboard;
  NSDragOperation sourceDragMask = sender.draggingSourceOperationMask;

  [self emitDragEvent:DragEnter draggingInfo:sender];

  if ([pboard availableTypeFromArray:self.registeredDraggedTypes]) {
    if (sourceDragMask & NSDragOperationLink) {
      return NSDragOperationLink;
    } else if (sourceDragMask & NSDragOperationCopy) {
      return NSDragOperationCopy;
    }
  }
  return NSDragOperationNone;
}

- (void)draggingExited:(id<NSDraggingInfo>)sender
{
  [self emitDragEvent:DragLeave draggingInfo:sender];
}

- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
  [self emitDragEvent:Drop draggingInfo:sender];
  return YES;
}


#pragma mark - Mouse Events

enum MouseEventType {
  MouseEnter,
  MouseLeave,
  Click,
  DoubleClick,
  AuxClick,
};

- (void)emitMouseEvent:(MouseEventType)eventType button:(int)button
{
  if (!_eventEmitter) {
    return;
  }

  NSPoint locationInWindow = self.window.mouseLocationOutsideOfEventStream;
  NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil];

  NSEventModifierFlags modifierFlags = self.window.currentEvent.modifierFlags;

  MouseEvent mouseEvent = {
    .clientX = locationInView.x,
    .clientY = locationInView.y,
    .screenX = locationInWindow.x,
    .screenY = locationInWindow.y,
    .altKey = static_cast<bool>(modifierFlags & NSEventModifierFlagOption),
    .ctrlKey = static_cast<bool>(modifierFlags & NSEventModifierFlagControl),
    .shiftKey = static_cast<bool>(modifierFlags & NSEventModifierFlagShift),
    .metaKey = static_cast<bool>(modifierFlags & NSEventModifierFlagCommand),
    .button = button,
  };

  switch (eventType) {
    case MouseEnter:
      _eventEmitter->onMouseEnter(mouseEvent);
      break;

    case MouseLeave:
      _eventEmitter->onMouseLeave(mouseEvent);
      break;

    case Click:
      _eventEmitter->onClick(mouseEvent);
      break;

    case DoubleClick:
      _eventEmitter->onDoubleClick(mouseEvent);
      break;

    case AuxClick:
      _eventEmitter->onAuxClick(mouseEvent);
      break;
  }
}

- (void)emitMouseEvent:(MouseEventType)eventType
{
  [self emitMouseEvent:eventType button:0];
}

- (void)updateMouseOverIfNeeded
{
  // When an enclosing scrollview is scrolled using the scrollWheel or trackpad,
  // the mouseExited: event does not get called on the view where mouseEntered: was previously called.
  // This creates an unnatural pairing of mouse enter and exit events and can cause problems.
  // We therefore explicitly check for this here and handle them by calling the appropriate callbacks.

  BOOL hasMouseOver = _hasMouseOver;
  NSPoint locationInWindow = self.window.mouseLocationOutsideOfEventStream;
  NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil];
  BOOL insideBounds = NSPointInRect(locationInView, self.visibleRect);

  // On macOS 14+ visibleRect can be larger than the view bounds
  insideBounds &= NSPointInRect(locationInView, self.bounds);

  if (hasMouseOver && !insideBounds) {
    hasMouseOver = NO;
  } else if (!hasMouseOver && insideBounds) {
    // The window's frame view must be used for hit testing against `locationInWindow`
    NSView *hitView = [self.window.contentView.superview hitTest:locationInWindow];
    hasMouseOver = [hitView isDescendantOf:self];
  }

  if (hasMouseOver != _hasMouseOver) {
    _hasMouseOver = hasMouseOver;
    [self emitMouseEvent:hasMouseOver ? MouseEnter : MouseLeave];
  }
}

- (void)updateClipViewBoundsObserverIfNeeded
{
  // Subscribe to view bounds changed notification so that the view can be notified when a
  // scroll event occurs either due to trackpad/gesture based scrolling or a scrollwheel event
  // both of which would not cause the mouseExited to be invoked.

  NSClipView *clipView = self.window ? self.enclosingScrollView.contentView : nil;

  BOOL hasMouseEventHandler =
    _props->hostPlatformEvents[HostPlatformViewEvents::Offset::MouseEnter] ||
    _props->hostPlatformEvents[HostPlatformViewEvents::Offset::MouseLeave];

  if (_hasClipViewBoundsObserver && (!clipView || !hasMouseEventHandler)) {
    _hasClipViewBoundsObserver = NO;
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:NSViewBoundsDidChangeNotification
                                                  object:nil];
  } else if (!_hasClipViewBoundsObserver && clipView && hasMouseEventHandler) {
    _hasClipViewBoundsObserver = YES;
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(updateMouseOverIfNeeded)
                                                 name:NSViewBoundsDidChangeNotification
                                               object:clipView];
    [self updateMouseOverIfNeeded];
  }
}

- (void)viewDidMoveToWindow
{
  [self updateClipViewBoundsObserverIfNeeded];
  [super viewDidMoveToWindow];
}

- (void)updateTrackingAreas
{
  BOOL hasMouseEventHandler =
    _props->hostPlatformEvents[HostPlatformViewEvents::Offset::MouseEnter] ||
    _props->hostPlatformEvents[HostPlatformViewEvents::Offset::MouseLeave];
  BOOL wouldRecreateIdenticalTrackingArea =
    hasMouseEventHandler && _trackingArea && NSEqualRects(self.bounds, [_trackingArea rect]);

  if (!wouldRecreateIdenticalTrackingArea) {
    [self removeTrackingArea:_trackingArea];
    if (hasMouseEventHandler) {
      _trackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds
                                                   options:NSTrackingActiveAlways|NSTrackingMouseEnteredAndExited
                                                     owner:self
                                                  userInfo:nil];
      [self addTrackingArea:_trackingArea];
      [self updateMouseOverIfNeeded];
    }
  }

  [super updateTrackingAreas];
}

- (void)mouseEntered:(NSEvent *)event
{
  if (_hasMouseOver) {
    return;
  }

  // The window's frame view must be used for hit testing against `locationInWindow`
  NSView *hitView = [self.window.contentView.superview hitTest:event.locationInWindow];
  if (![hitView isDescendantOf:self]) {
    return;
  }

  _hasMouseOver = YES;
  [self emitMouseEvent:MouseEnter];
}

- (void)mouseExited:(NSEvent *)event
{
  if (!_hasMouseOver) {
    return;
  }

  _hasMouseOver = NO;
  [self emitMouseEvent:MouseLeave];
}

- (void)mouseUp:(NSEvent *)event
{
  BOOL hasDoubleClickEventHandler = _props->hostPlatformEvents[HostPlatformViewEvents::Offset::DoubleClick];
  BOOL hasClickEventHandler = _props->hostPlatformEvents[HostPlatformViewEvents::Offset::Click];
  if (hasDoubleClickEventHandler && event.clickCount == 2) {
    [self emitMouseEvent:DoubleClick];
  } else if (hasClickEventHandler && event.clickCount == 1) {
    [self emitMouseEvent:Click];
  } else {
    [super mouseUp:event];
  }
}

- (void)rightMouseDown:(NSEvent *)event
{
  // Accept rightMouseDown to prevent the default NSView behavior of passing it
  // up the responder chain (which can trigger a context menu modal loop that
  // consumes rightMouseUp).
  BOOL hasAuxClickEventHandler = _props->hostPlatformEvents[HostPlatformViewEvents::Offset::AuxClick];
  if (!hasAuxClickEventHandler) {
    [super rightMouseDown:event];
  }
}

- (void)rightMouseUp:(NSEvent *)event
{
  BOOL hasAuxClickEventHandler = _props->hostPlatformEvents[HostPlatformViewEvents::Offset::AuxClick];
  if (hasAuxClickEventHandler) {
    [self emitMouseEvent:AuxClick button:2];
  } else {
    [super rightMouseUp:event];
  }
}

- (void)otherMouseDown:(NSEvent *)event
{
  // Accept otherMouseDown so that otherMouseUp is delivered to this view.
  BOOL hasAuxClickEventHandler = _props->hostPlatformEvents[HostPlatformViewEvents::Offset::AuxClick];
  if (!hasAuxClickEventHandler) {
    [super otherMouseDown:event];
  }
}

- (void)otherMouseUp:(NSEvent *)event
{
  BOOL hasAuxClickEventHandler = _props->hostPlatformEvents[HostPlatformViewEvents::Offset::AuxClick];
  if (hasAuxClickEventHandler) {
    [self emitMouseEvent:AuxClick button:1];
  } else {
    [super otherMouseUp:event];
  }
}
#endif // macOS]

- (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point
{
  return _eventEmitter;
}

- (NSString *)componentViewName_DO_NOT_USE_THIS_IS_BROKEN
{
  return RCTNSStringFromString([[self class] componentDescriptorProvider].name);
}

- (BOOL)wantsUpdateLayer
{
  return YES;
}

@end

#ifdef __cplusplus
extern "C" {
#endif

// Can't the import generated Plugin.h because plugins are not in this BUCK target
Class<RCTComponentViewProtocol> RCTViewCls(void);

#ifdef __cplusplus
}
#endif

Class<RCTComponentViewProtocol> RCTViewCls(void)
{
  return RCTViewComponentView.class;
}
