react-native-reanimated#useAnimatedReaction TypeScript Examples

The following examples show how to use react-native-reanimated#useAnimatedReaction. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: useAnimatedPath.ts    From react-native-wagmi-charts with MIT License 6 votes vote down vote up
export default function useAnimatedPath({
  enabled = true,
  path,
}: {
  enabled?: boolean;
  path: string;
}) {
  const transition = useSharedValue(0);

  const previousPath = usePrevious(path);

  useAnimatedReaction(
    () => {
      return path;
    },
    (_, previous) => {
      if (previous) {
        transition.value = 0;
        transition.value = withTiming(1);
      }
    },
    [path]
  );

  const animatedProps = useAnimatedProps(() => {
    let d = path || '';
    if (previousPath && enabled) {
      const pathInterpolator = interpolatePath(previousPath, path, null);
      d = pathInterpolator(transition.value);
    }
    return {
      d,
    };
  });

  return { animatedProps };
}
Example #2
Source File: AnimatedText.tsx    From react-native-wagmi-charts with MIT License 6 votes vote down vote up
AnimatedText = ({ text, style }: AnimatedTextProps) => {
  const inputRef = React.useRef<any>(null); // eslint-disable-line @typescript-eslint/no-explicit-any

  if (Platform.OS === 'web') {
    // For some reason, the worklet reaction evaluates upfront regardless of any
    // conditionals within it, causing Android to crash upon the invokation of `setNativeProps`.
    // We are going to break the rules of hooks here so it doesn't invoke `useAnimatedReaction`
    // for platforms outside of the web.

    // eslint-disable-next-line react-hooks/rules-of-hooks
    useAnimatedReaction(
      () => {
        return text.value;
      },
      (data, prevData) => {
        if (data !== prevData && inputRef.current) {
          inputRef.current.value = data;
        }
      }
    );
  }
  const animatedProps = useAnimatedProps(() => {
    return {
      text: text.value,
      // Here we use any because the text prop is not available in the type
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
    } as any;
  });
  return (
    <AnimatedTextInput
      underlineColorAndroid="transparent"
      editable={false}
      ref={Platform.select({ web: inputRef })}
      value={text.value}
      style={[styles.text, style]}
      animatedProps={animatedProps}
    />
  );
}
Example #3
Source File: IconDot.tsx    From curved-bottom-navigation-bar with MIT License 5 votes vote down vote up
IconDotComponent = (props: IconDotProps) => {
  // props
  const { index, selectedIndex, children } = props;

  // reanimated
  const progress = useSharedValue(0);
  useAnimatedReaction(
    () => selectedIndex.value === index,
    (result, prevValue) => {
      if (result !== prevValue) {
        progress.value = sharedTiming(result ? 1 : 0);
      }
    }
  );
  const opacity = useInterpolate(progress, [0, 0.6, 1], [0, 0, 1]);
  const scale = useInterpolate(progress, [0, 1], [0.2, 1]);

  // reanimated style
  const style = useAnimatedStyle(() => ({
    position: 'absolute',
    opacity: opacity.value,
    transform: [{ scale: scale.value }],
  }));

  // render
  return <Animated.View style={[style]}>{children}</Animated.View>;
}
Example #4
Source File: Context.tsx    From react-native-wagmi-charts with MIT License 5 votes vote down vote up
export function LineChartProvider({
  children,
  data = [],
  yRange,
  onCurrentIndexChange,
  xLength,
}: LineChartProviderProps) {
  const currentX = useSharedValue(-1);
  const currentIndex = useSharedValue(-1);
  const isActive = useSharedValue(false);

  const domain = React.useMemo(
    () => getDomain(Array.isArray(data) ? data : Object.values(data)[0]),
    [data]
  );

  const contextValue = React.useMemo<TLineChartContext>(() => {
    const values = lineChartDataPropToArray(data).map(({ value }) => value);

    return {
      currentX,
      currentIndex,
      isActive,
      domain,
      yDomain: {
        min: yRange?.min ?? Math.min(...values),
        max: yRange?.max ?? Math.max(...values),
      },
      xLength:
        xLength ?? (Array.isArray(data) ? data : Object.values(data)[0]).length,
    };
  }, [
    currentIndex,
    currentX,
    data,
    domain,
    isActive,
    yRange?.max,
    yRange?.min,
    xLength,
  ]);

  useAnimatedReaction(
    () => currentIndex.value,
    (x, prevX) => {
      if (x !== -1 && x !== prevX && onCurrentIndexChange) {
        runOnJS(onCurrentIndexChange)(x);
      }
    }
  );

  return (
    <LineChartDataProvider data={data}>
      <LineChartContext.Provider value={contextValue}>
        {children}
      </LineChartContext.Provider>
    </LineChartDataProvider>
  );
}
Example #5
Source File: AnimatedTabBar.tsx    From curved-bottom-navigation-bar with MIT License 4 votes vote down vote up
AnimatedTabBarComponent = (props: AnimatedTabBarProps) => {
  // props
  const {
    navigation,
    tabs,
    descriptors,
    state,
    duration = DEFAULT_ITEM_ANIMATION_DURATION,
    barColor = TAB_BAR_COLOR,
    dotSize = SIZE_DOT,
    barHeight = TAB_BAR_HEIGHT,
    dotColor = TAB_BAR_COLOR,
    titleShown = false,
    barWidth,
  } = props;

  // variables

  const {
    routes,
    index: navigationIndex,
    key: navigationKey,
  } = useMemo(() => {
    return state;
  }, [state]);

  // reanimated
  const selectedIndex = useSharedValue(0);

  // callbacks
  const getRouteTitle = useCallback(
    (route: Route<string>) => {
      const { options } = descriptors[route.key];
      // eslint-disable-next-line no-nested-ternary
      return options.tabBarLabel !== undefined &&
        typeof options.tabBarLabel === 'string'
        ? options.tabBarLabel
        : options.title !== undefined
        ? options.title
        : route.name;
    },
    [descriptors]
  );

  const getRouteTabConfigs = useCallback(
    (route: Route<string>) => {
      return tabs[route.name];
    },
    [tabs]
  );

  const getRoutes = useCallback(() => {
    return routes.map((route) => ({
      key: route.key,
      title: getRouteTitle(route),
      ...getRouteTabConfigs(route),
    }));
  }, [routes, getRouteTitle, getRouteTabConfigs]);

  const handleSelectedIndexChange = useCallback(
    (index: number) => {
      const { key, name } = routes[index];
      const event = navigation.emit({
        type: 'tabPress',
        target: key,
        canPreventDefault: true,
      });

      if (!event.defaultPrevented) {
        navigation.dispatch({
          ...CommonActions.navigate(name),
          target: navigationKey,
        });
      }
    },
    [routes, navigation, navigationKey]
  );

  // effects
  useEffect(() => {
    selectedIndex.value = navigationIndex;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [navigationIndex]);

  useAnimatedReaction(
    () => selectedIndex.value,
    (nextSelected, prevSelected) => {
      if (nextSelected !== prevSelected) {
        runOnJS(handleSelectedIndexChange)(nextSelected);
      }
    },
    [selectedIndex, handleSelectedIndexChange]
  );

  // render
  return (
    <CurvedTabBar
      isRtl={I18nManager.isRTL}
      barWidth={barWidth}
      titleShown={titleShown}
      dotColor={dotColor}
      barHeight={barHeight}
      dotSize={dotSize}
      tabBarColor={barColor}
      selectedIndex={selectedIndex}
      navigationIndex={navigationIndex}
      routes={getRoutes()}
      duration={duration}
    />
  );
}
Example #6
Source File: ButtonTabItem.tsx    From curved-bottom-navigation-bar with MIT License 4 votes vote down vote up
ButtonTabItemComponent = (props: TabBarItemProps) => {
  // props
  const {
    index,
    selectedIndex,
    countTab,
    indexAnimated,
    width,
    icon,
    renderTitle,
    title,
    titleShown,
    focused,
  } = props;
  // reanimated
  const {bottom} = useSafeAreaInsets();
  const isActive = useDerivedValue(() => sharedRound(indexAnimated.value));
  const progress = useSharedValue(0);

  const opacity = useInterpolate(progress, [0, 0.8], [1, 0]);
  const translateY = useInterpolate(progress, [0, 0.4], [0, 10]);
  const scale = useInterpolate(progress, [0, 1], [1, 0.5]);

  // func
  const _onPress = useCallback(() => {
    selectedIndex.value = index;
  }, [index, selectedIndex]);

  // effect
  useAnimatedReaction(
    () => isActive.value === index,
    (result, prevValue) => {
      if (result !== prevValue) {
        progress.value = sharedTiming(result ? 1 : 0);
      }
    },
  );

  // reanimated style
  const containerIconStyle = useAnimatedStyle(() => ({
    opacity: opacity.value,
    justifyContent: 'center',
    alignItems: 'center',
    transform: [
      {
        translateY: translateY.value,
      },
    ],
  }));

  const titleStyle = useAnimatedStyle(() => ({
    transform: [{scale: scale.value}],
  }));

  const buttonTab = useMemo(
    () => ({
      width: width / countTab,
      paddingBottom: bottom,
    }),
    [width, countTab, bottom],
  );

  // render
  const renderIcon = useCallback(() => {
    return icon({progress, focused});
  }, [focused, icon, progress]);

  const _renderTitle = useCallback(() => {
    return renderTitle?.({progress, focused, title: title ?? ''});
  }, [focused, progress, renderTitle, title]);

  const showTitle = useCallback(() => {
    if (typeof renderTitle === 'function') {
      return _renderTitle();
    }
    return (
      <Animated.Text
        style={[styles.title, titleStyle]}
        allowFontScaling={false}
        numberOfLines={1}>
        {title ?? ''}
      </Animated.Text>
    );
  }, [_renderTitle, renderTitle, title, titleStyle]);

  // render
  return (
    <TouchableOpacity onPress={_onPress} activeOpacity={0.7}>
      <View style={[styles.buttonTab, buttonTab]}>
        <Animated.View style={[containerIconStyle]}>
          {renderIcon()}
          {titleShown ? showTitle() : null}
        </Animated.View>
      </View>
    </TouchableOpacity>
  );
}
Example #7
Source File: ButtonTabItem.tsx    From curved-bottom-navigation-bar with MIT License 4 votes vote down vote up
ButtonTabItemComponent = (props: TabBarItemProps) => {
  // props
  const {
    index,
    selectedIndex,
    countTab,
    indexAnimated,
    width,
    icon,
    renderTitle,
    title,
    titleShown,
    focused,
  } = props;
  // reanimated
  const { bottom } = useSafeAreaInsets();
  const isActive = useDerivedValue(() => sharedRound(indexAnimated.value));
  const progress = useSharedValue(0);

  const opacity = useInterpolate(progress, [0, 0.8], [1, 0]);
  const translateY = useInterpolate(progress, [0, 0.4], [0, 10]);
  const scale = useInterpolate(progress, [0, 1], [1, 0.5]);

  // func
  const _onPress = useCallback(() => {
    selectedIndex.value = index;
  }, [index, selectedIndex]);

  // effect
  useAnimatedReaction(
    () => isActive.value === index,
    (result, prevValue) => {
      if (result !== prevValue) {
        progress.value = sharedTiming(result ? 1 : 0);
      }
    }
  );

  // reanimated style
  const containerIconStyle = useAnimatedStyle(() => ({
    opacity: opacity.value,
    justifyContent: 'center',
    alignItems: 'center',
    transform: [
      {
        translateY: translateY.value,
      },
    ],
  }));

  const titleStyle = useAnimatedStyle(() => ({
    transform: [{ scale: scale.value }],
  }));

  const buttonTab = useMemo(
    () => ({
      width: width / countTab,
      paddingBottom: bottom,
    }),
    [width, countTab, bottom]
  );

  // render
  const renderIcon = useCallback(() => {
    return icon({ progress, focused });
  }, [focused, icon, progress]);

  const _renderTitle = useCallback(() => {
    return renderTitle?.({ progress, focused, title: title ?? '' });
  }, [focused, progress, renderTitle, title]);

  const showTitle = useCallback(() => {
    if (typeof renderTitle === 'function') {
      return _renderTitle();
    }
    return (
      <Animated.Text
        style={[styles.title, titleStyle]}
        allowFontScaling={false}
        numberOfLines={1}
      >
        {title ?? ''}
      </Animated.Text>
    );
  }, [_renderTitle, renderTitle, title, titleStyle]);

  // render
  return (
    <TouchableOpacity onPress={_onPress} activeOpacity={0.7}>
      <View style={[styles.buttonTab, buttonTab]}>
        <Animated.View style={[containerIconStyle]}>
          {renderIcon()}
          {titleShown ? showTitle() : null}
        </Animated.View>
      </View>
    </TouchableOpacity>
  );
}
Example #8
Source File: Crosshair.tsx    From react-native-wagmi-charts with MIT License 4 votes vote down vote up
export function CandlestickChartCrosshair({
  color,
  onCurrentXChange,
  children,
  horizontalCrosshairProps = {},
  verticalCrosshairProps = {},
  lineProps = {},
  ...props
}: CandlestickChartCrosshairProps) {
  const { width, height } = React.useContext(CandlestickChartDimensionsContext);
  const { currentX, currentY, step } = useCandlestickChart();

  const tooltipPosition = useSharedValue<'left' | 'right'>('left');

  const opacity = useSharedValue(0);
  const onGestureEvent = useAnimatedGestureHandler<
    GestureEvent<LongPressGestureHandlerEventPayload>
  >({
    onActive: ({ x, y }) => {
      const boundedX = x <= width - 1 ? x : width - 1;
      if (boundedX < 100) {
        tooltipPosition.value = 'right';
      } else {
        tooltipPosition.value = 'left';
      }
      opacity.value = 1;
      currentY.value = clamp(y, 0, height);
      currentX.value = boundedX - (boundedX % step) + step / 2;
    },
    onEnd: () => {
      opacity.value = 0;
      currentY.value = -1;
      currentX.value = -1;
    },
  });
  const horizontal = useAnimatedStyle(() => ({
    opacity: opacity.value,
    transform: [{ translateY: currentY.value }],
  }));
  const vertical = useAnimatedStyle(() => ({
    opacity: opacity.value,
    transform: [{ translateX: currentX.value }],
  }));

  useAnimatedReaction(
    () => currentX.value,
    (data, prevData) => {
      if (data !== -1 && data !== prevData && onCurrentXChange) {
        runOnJS(onCurrentXChange)(data);
      }
    }
  );

  return (
    <LongPressGestureHandler
      minDurationMs={0}
      maxDist={999999}
      onGestureEvent={onGestureEvent}
      {...props}
    >
      <Animated.View style={StyleSheet.absoluteFill}>
        <Animated.View
          style={[StyleSheet.absoluteFill, horizontal]}
          {...horizontalCrosshairProps}
        >
          <CandlestickChartLine color={color} x={width} y={0} {...lineProps} />
          <CandlestickChartCrosshairTooltipContext.Provider
            value={{ position: tooltipPosition }}
          >
            {children}
          </CandlestickChartCrosshairTooltipContext.Provider>
        </Animated.View>
        <Animated.View
          style={[StyleSheet.absoluteFill, vertical]}
          {...verticalCrosshairProps}
        >
          <CandlestickChartLine color={color} x={0} y={height} {...lineProps} />
        </Animated.View>
      </Animated.View>
    </LongPressGestureHandler>
  );
}
Example #9
Source File: ImageTransformer.tsx    From react-native-gallery-toolkit with MIT License 4 votes vote down vote up
ImageTransformer = React.memo<ImageTransformerProps>(
  ({
    outerGestureHandlerRefs = [],
    source,
    width,
    height,
    onStateChange = workletNoop,
    renderImage,
    windowDimensions = Dimensions.get('window'),
    isActive,
    outerGestureHandlerActive,
    style,
    onTap = workletNoop,
    onDoubleTap = workletNoop,
    onInteraction = workletNoop,
    DOUBLE_TAP_SCALE = 3,
    MAX_SCALE = 3,
    MIN_SCALE = 0.7,
    OVER_SCALE = 0.5,
    timingConfig = defaultTimingConfig,
    springConfig = defaultSpringConfig,
    enabled = true,
    ImageComponent = Image,
  }) => {
    // fixGestureHandler();

    assertWorklet(onStateChange);
    assertWorklet(onTap);
    assertWorklet(onDoubleTap);
    assertWorklet(onInteraction);

    if (typeof source === 'undefined') {
      throw new Error(
        'ImageTransformer: either source or uri should be passed to display an image',
      );
    }

    const imageSource = useMemo(
      () =>
        typeof source === 'string'
          ? {
              uri: source,
            }
          : source,
      [source],
    );

    const interactionsEnabled = useSharedValue(false);
    const setInteractionsEnabled = useCallback((value: boolean) => {
      interactionsEnabled.value = value;
    }, []);
    const onLoadImageSuccess = useCallback(() => {
      setInteractionsEnabled(true);
    }, []);

    // HACK ALERT
    // we disable pinch handler in order to trigger onFinish
    // in case user releases one finger
    const [pinchEnabled, setPinchEnabledState] = useState(true);
    useEffect(() => {
      if (!pinchEnabled) {
        setPinchEnabledState(true);
      }
    }, [pinchEnabled]);
    const disablePinch = useCallback(() => {
      setPinchEnabledState(false);
    }, []);
    // HACK ALERT END

    const pinchRef = useRef(null);
    const panRef = useRef(null);
    const tapRef = useRef(null);
    const doubleTapRef = useRef(null);

    const panState = useSharedValue<State>(State.UNDETERMINED);
    const pinchState = useSharedValue<State>(State.UNDETERMINED);

    const scale = useSharedValue(1);
    const scaleOffset = useSharedValue(1);
    const translation = vectors.useSharedVector(0, 0);
    const panVelocity = vectors.useSharedVector(0, 0);
    const scaleTranslation = vectors.useSharedVector(0, 0);
    const offset = vectors.useSharedVector(0, 0);

    const canvas = useMemo(
      () =>
        vectors.create(
          windowDimensions.width,
          windowDimensions.height,
        ),
      [windowDimensions.width, windowDimensions.height],
    );
    const targetWidth = windowDimensions.width;
    const scaleFactor = width / targetWidth;
    const targetHeight = height / scaleFactor;
    const image = useMemo(
      () => vectors.create(targetWidth, targetHeight),
      [targetHeight, targetWidth],
    );

    const canPanVertically = useDerivedValue(() => {
      return windowDimensions.height < targetHeight * scale.value;
    }, []);

    const resetSharedState = useWorkletCallback(
      (animated?: boolean) => {
        'worklet';

        if (animated) {
          scale.value = withTiming(1, timingConfig);
          scaleOffset.value = 1;

          vectors.set(offset, () => withTiming(0, timingConfig));
        } else {
          scale.value = 1;
          scaleOffset.value = 1;
          vectors.set(translation, 0);
          vectors.set(scaleTranslation, 0);
          vectors.set(offset, 0);
        }
      },
      [timingConfig],
    );

    const maybeRunOnEnd = useWorkletCallback(() => {
      'worklet';

      const target = vectors.create(0, 0);

      const fixedScale = clamp(MIN_SCALE, scale.value, MAX_SCALE);
      const scaledImage = image.y * fixedScale;
      const rightBoundary = (canvas.x / 2) * (fixedScale - 1);

      let topBoundary = 0;

      if (canvas.y < scaledImage) {
        topBoundary = Math.abs(scaledImage - canvas.y) / 2;
      }

      const maxVector = vectors.create(rightBoundary, topBoundary);
      const minVector = vectors.invert(maxVector);

      if (!canPanVertically.value) {
        offset.y.value = withSpring(target.y, springConfig);
      }

      // we should handle this only if pan or pinch handlers has been used already
      if (
        (checkIsNotUsed(panState) || checkIsNotUsed(pinchState)) &&
        pinchState.value !== State.CANCELLED
      ) {
        return;
      }

      if (
        vectors.eq(offset, 0) &&
        vectors.eq(translation, 0) &&
        vectors.eq(scaleTranslation, 0) &&
        scale.value === 1
      ) {
        // we don't need to run any animations
        return;
      }

      if (scale.value <= 1) {
        // just center it
        vectors.set(offset, () => withTiming(0, timingConfig));
        return;
      }

      vectors.set(
        target,
        vectors.clamp(offset, minVector, maxVector),
      );

      const deceleration = 0.9915;

      const isInBoundaryX = target.x === offset.x.value;
      const isInBoundaryY = target.y === offset.y.value;

      if (isInBoundaryX) {
        if (
          Math.abs(panVelocity.x.value) > 0 &&
          scale.value <= MAX_SCALE
        ) {
          offset.x.value = withDecay({
            velocity: panVelocity.x.value,
            clamp: [minVector.x, maxVector.x],
            deceleration,
          });
        }
      } else {
        offset.x.value = withSpring(target.x, springConfig);
      }

      if (isInBoundaryY) {
        if (
          Math.abs(panVelocity.y.value) > 0 &&
          scale.value <= MAX_SCALE &&
          offset.y.value !== minVector.y &&
          offset.y.value !== maxVector.y
        ) {
          offset.y.value = withDecay({
            velocity: panVelocity.y.value,
            clamp: [minVector.y, maxVector.y],
            deceleration,
          });
        }
      } else {
        offset.y.value = withSpring(target.y, springConfig);
      }
    }, [
      MIN_SCALE,
      MAX_SCALE,
      image,
      canvas,
      springConfig,
      timingConfig,
    ]);

    const onPanEvent = useAnimatedGestureHandler<
      PanGestureHandlerGestureEvent,
      {
        panOffset: vectors.Vector<number>;
        pan: vectors.Vector<number>;
      }
    >({
      onInit: (_, ctx) => {
        ctx.panOffset = vectors.create(0, 0);
      },

      shouldHandleEvent: () => {
        return (
          scale.value > 1 &&
          (typeof outerGestureHandlerActive !== 'undefined'
            ? !outerGestureHandlerActive.value
            : true) &&
          interactionsEnabled.value
        );
      },

      beforeEach: (evt, ctx) => {
        ctx.pan = vectors.create(evt.translationX, evt.translationY);
        const velocity = vectors.create(evt.velocityX, evt.velocityY);

        vectors.set(panVelocity, velocity);
      },

      onStart: (_, ctx) => {
        cancelAnimation(offset.x);
        cancelAnimation(offset.y);
        ctx.panOffset = vectors.create(0, 0);
        onInteraction('pan');
      },

      onActive: (evt, ctx) => {
        panState.value = evt.state;

        if (scale.value > 1) {
          if (evt.numberOfPointers > 1) {
            // store pan offset during the pan with two fingers (during the pinch)
            vectors.set(ctx.panOffset, ctx.pan);
          } else {
            // subtract the offset and assign fixed pan
            const nextTranslate = vectors.add(
              ctx.pan,
              vectors.invert(ctx.panOffset),
            );
            translation.x.value = nextTranslate.x;

            if (canPanVertically.value) {
              translation.y.value = nextTranslate.y;
            }
          }
        }
      },

      onEnd: (evt, ctx) => {
        panState.value = evt.state;

        vectors.set(ctx.panOffset, 0);
        vectors.set(offset, vectors.add(offset, translation));
        vectors.set(translation, 0);

        maybeRunOnEnd();

        vectors.set(panVelocity, 0);
      },
    });

    useAnimatedReaction(
      () => {
        if (typeof isActive === 'undefined') {
          return true;
        }

        return isActive.value;
      },
      (currentActive) => {
        if (!currentActive) {
          resetSharedState();
        }
      },
    );

    const onScaleEvent = useAnimatedGestureHandler<
      PinchGestureHandlerGestureEvent,
      {
        origin: vectors.Vector<number>;
        adjustFocal: vectors.Vector<number>;
        gestureScale: number;
        nextScale: number;
      }
    >({
      onInit: (_, ctx) => {
        ctx.origin = vectors.create(0, 0);
        ctx.gestureScale = 1;
        ctx.adjustFocal = vectors.create(0, 0);
      },

      shouldHandleEvent: (evt) => {
        return (
          evt.numberOfPointers === 2 &&
          (typeof outerGestureHandlerActive !== 'undefined'
            ? !outerGestureHandlerActive.value
            : true) &&
          interactionsEnabled.value
        );
      },

      beforeEach: (evt, ctx) => {
        // calculate the overall scale value
        // also limits this.event.scale
        ctx.nextScale = clamp(
          evt.scale * scaleOffset.value,
          MIN_SCALE,
          MAX_SCALE + OVER_SCALE,
        );

        if (
          ctx.nextScale > MIN_SCALE &&
          ctx.nextScale < MAX_SCALE + OVER_SCALE
        ) {
          ctx.gestureScale = evt.scale;
        }

        // this is just to be able to use with vectors
        const focal = vectors.create(evt.focalX, evt.focalY);
        const CENTER = vectors.divide(canvas, 2);

        // since it works even when you release one finger
        if (evt.numberOfPointers === 2) {
          // focal with translate offset
          // it alow us to scale into different point even then we pan the image
          ctx.adjustFocal = vectors.sub(
            focal,
            vectors.add(CENTER, offset),
          );
        } else if (
          evt.state === State.ACTIVE &&
          evt.numberOfPointers !== 2
        ) {
          runOnJS(disablePinch)();
        }
      },

      afterEach: (evt, ctx) => {
        if (
          evt.state === State.END ||
          evt.state === State.CANCELLED
        ) {
          return;
        }

        scale.value = ctx.nextScale;
      },

      onStart: (_, ctx) => {
        onInteraction('scale');
        cancelAnimation(offset.x);
        cancelAnimation(offset.y);
        vectors.set(ctx.origin, ctx.adjustFocal);
      },

      onActive: (evt, ctx) => {
        pinchState.value = evt.state;

        const pinch = vectors.sub(ctx.adjustFocal, ctx.origin);

        const nextTranslation = vectors.add(
          pinch,
          ctx.origin,
          vectors.multiply(-1, ctx.gestureScale, ctx.origin),
        );

        vectors.set(scaleTranslation, nextTranslation);
      },

      onFinish: (evt, ctx) => {
        // reset gestureScale value
        ctx.gestureScale = 1;
        pinchState.value = evt.state;
        // store scale value
        scaleOffset.value = scale.value;

        vectors.set(offset, vectors.add(offset, scaleTranslation));
        vectors.set(scaleTranslation, 0);

        if (scaleOffset.value < 1) {
          // make sure we don't add stuff below the 1
          scaleOffset.value = 1;

          // this runs the timing animation
          scale.value = withTiming(1, timingConfig);
        } else if (scaleOffset.value > MAX_SCALE) {
          scaleOffset.value = MAX_SCALE;
          scale.value = withTiming(MAX_SCALE, timingConfig);
        }

        maybeRunOnEnd();
      },
    });

    const onTapEvent = useAnimatedGestureHandler({
      shouldHandleEvent: (evt) => {
        return (
          evt.numberOfPointers === 1 &&
          (typeof outerGestureHandlerActive !== 'undefined'
            ? !outerGestureHandlerActive.value
            : true) &&
          interactionsEnabled.value
        );
      },

      onStart: () => {
        cancelAnimation(offset.x);
        cancelAnimation(offset.y);
      },

      onActive: () => {
        onTap(scale.value > 1);
      },

      onEnd: () => {
        maybeRunOnEnd();
      },
    });

    const handleScaleTo = useWorkletCallback(
      (x: number, y: number) => {
        'worklet';

        scale.value = withTiming(DOUBLE_TAP_SCALE, timingConfig);
        scaleOffset.value = DOUBLE_TAP_SCALE;

        const targetImageSize = vectors.multiply(
          image,
          DOUBLE_TAP_SCALE,
        );

        const CENTER = vectors.divide(canvas, 2);
        const imageCenter = vectors.divide(image, 2);

        const focal = vectors.create(x, y);

        const origin = vectors.multiply(
          -1,
          vectors.sub(vectors.divide(targetImageSize, 2), CENTER),
        );

        const koef = vectors.sub(
          vectors.multiply(vectors.divide(1, imageCenter), focal),
          1,
        );

        const target = vectors.multiply(origin, koef);

        if (targetImageSize.y < canvas.y) {
          target.y = 0;
        }

        offset.x.value = withTiming(target.x, timingConfig);
        offset.y.value = withTiming(target.y, timingConfig);
      },
      [DOUBLE_TAP_SCALE, timingConfig, image, canvas],
    );

    const onDoubleTapEvent = useAnimatedGestureHandler<
      TapGestureHandlerGestureEvent,
      {}
    >({
      shouldHandleEvent: (evt) => {
        return (
          evt.numberOfPointers === 1 &&
          (typeof outerGestureHandlerActive !== 'undefined'
            ? !outerGestureHandlerActive.value
            : true) &&
          interactionsEnabled.value
        );
      },

      onActive: ({ x, y }) => {
        onDoubleTap(scale.value > 1);

        if (scale.value > 1) {
          resetSharedState(true);
        } else {
          handleScaleTo(x, y);
        }
      },
    });

    const animatedStyles = useAnimatedStyle<ViewStyle>(() => {
      const noOffset = offset.x.value === 0 && offset.y.value === 0;
      const noTranslation =
        translation.x.value === 0 && translation.y.value === 0;
      const noScaleTranslation =
        scaleTranslation.x.value === 0 &&
        scaleTranslation.y.value === 0;

      const isInactive =
        scale.value === 1 &&
        noOffset &&
        noTranslation &&
        noScaleTranslation;

      onStateChange(isInactive);

      return {
        transform: [
          {
            translateX:
              scaleTranslation.x.value +
              translation.x.value +
              offset.x.value,
          },
          {
            translateY:
              scaleTranslation.y.value +
              translation.y.value +
              offset.y.value,
          },
          { scale: scale.value },
        ],
      };
    }, []);

    return (
      <Animated.View
        style={[styles.container, { width: targetWidth }, style]}
      >
        <PinchGestureHandler
          enabled={enabled && pinchEnabled}
          ref={pinchRef}
          onGestureEvent={onScaleEvent}
          simultaneousHandlers={[
            panRef,
            tapRef,
            ...outerGestureHandlerRefs,
          ]}
        >
          <Animated.View style={styles.fill}>
            <PanGestureHandler
              enabled={enabled}
              ref={panRef}
              minDist={4}
              avgTouches
              simultaneousHandlers={[
                pinchRef,
                tapRef,
                ...outerGestureHandlerRefs,
              ]}
              onGestureEvent={onPanEvent}
            >
              <Animated.View style={styles.fill}>
                <TapGestureHandler
                  enabled={enabled}
                  ref={tapRef}
                  numberOfTaps={1}
                  maxDeltaX={8}
                  maxDeltaY={8}
                  simultaneousHandlers={[
                    pinchRef,
                    panRef,
                    ...outerGestureHandlerRefs,
                  ]}
                  waitFor={doubleTapRef}
                  onGestureEvent={onTapEvent}
                >
                  <Animated.View style={[styles.fill]}>
                    <Animated.View style={styles.fill}>
                      <Animated.View style={styles.wrapper}>
                        <TapGestureHandler
                          enabled={enabled}
                          ref={doubleTapRef}
                          numberOfTaps={2}
                          maxDelayMs={140}
                          maxDeltaX={16}
                          maxDeltaY={16}
                          simultaneousHandlers={[
                            pinchRef,
                            panRef,
                            ...outerGestureHandlerRefs,
                          ]}
                          onGestureEvent={onDoubleTapEvent}
                        >
                          <Animated.View style={animatedStyles}>
                            {typeof renderImage === 'function' ? (
                              renderImage({
                                source: imageSource,
                                width: targetWidth,
                                height: targetHeight,
                                onLoad: onLoadImageSuccess,
                              })
                            ) : (
                              <ImageComponent
                                onLoad={onLoadImageSuccess}
                                source={imageSource}
                                style={{
                                  width: targetWidth,
                                  height: targetHeight,
                                }}
                              />
                            )}
                          </Animated.View>
                        </TapGestureHandler>
                      </Animated.View>
                    </Animated.View>
                  </Animated.View>
                </TapGestureHandler>
              </Animated.View>
            </PanGestureHandler>
          </Animated.View>
        </PinchGestureHandler>
      </Animated.View>
    );
  },
)