react-native-reanimated#Easing TypeScript Examples

The following examples show how to use react-native-reanimated#Easing. 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: AnimatedHelper.ts    From curved-bottom-navigation-bar with MIT License 6 votes vote down vote up
withSharedTransition = (
  value: Animated.SharedValue<boolean>,
  config: WithTimingConfig = {
    duration: 500,
    easing: Easing.bezier(0, 0.55, 0.45, 1),
  }
): Animated.SharedValue<number> => {
  'worklet';
  return useDerivedValue(() =>
    value.value ? withTiming(1, config) : withTiming(0, config)
  );
}
Example #2
Source File: useInit.ts    From react-native-gallery-toolkit with MIT License 6 votes vote down vote up
usedWorklets = {
  withTiming,
  withSpring,
  bezier: Easing.bezier,
  interpolate,
  withDecay,
  useAnimatedGestureHandler,
  ...usedVectors,
} as { [key: string]: any }
Example #3
Source File: useControls.tsx    From react-native-gallery-toolkit with MIT License 6 votes vote down vote up
export function useControls() {
  const controlsHidden = useSharedValue(false);

  const translateYConfig = {
    duration: 400,
    easing: Easing.bezier(0.33, 0.01, 0, 1),
  };

  const controlsStyles = useAnimatedStyle(() => {
    return {
      opacity: controlsHidden.value ? withTiming(0) : withTiming(1),
      transform: [
        {
          translateY: controlsHidden.value
            ? withTiming(-100, translateYConfig)
            : withTiming(0, translateYConfig),
        },
      ],
      position: 'absolute',
      top: 0,
      width: '100%',
      zIndex: 1,
    };
  });

  const setControlsHidden = useWorkletCallback((hidden: boolean) => {
    if (controlsHidden.value === hidden) {
      return;
    }

    controlsHidden.value = hidden;
  }, []);

  return {
    controlsHidden,
    controlsStyles,
    setControlsHidden,
  };
}
Example #4
Source File: animated-stroke.tsx    From react-native-checkbox-reanimated with MIT License 6 votes vote down vote up
AnimatedStroke = ({ progress, ...pathProps }: AnimatedStrokeProps) => {
  const [length, setLength] = useState(0)
  const ref = useRef<typeof AnimatedPath>(null)
  const animatedProps = useAnimatedProps(() => ({
    strokeDashoffset: Math.max(
      0,
      length - length * Easing.bezierFn(0.37, 0, 0.63, 1)(progress.value) - 0.1
    )
  }))

  return (
    <AnimatedPath
      animatedProps={animatedProps}
      // @ts-ignore
      onLayout={() => setLength(ref.current!.getTotalLength())}
      // @ts-ignore
      ref={ref}
      strokeDasharray={length}
      {...pathProps}
    />
  )
}
Example #5
Source File: RandomArc.tsx    From reanimated-arc with MIT License 6 votes vote down vote up
RandomArc = () => {
  const arcAngle = useRef(new Reanimated.Value(Math.random() * 360));
  const animate = () =>
    Reanimated.timing(arcAngle.current, {
      toValue: Math.random() * 360,
      easing: Easing.inOut(Easing.quad),
      duration: 1000,
    }).start();

  useEffect(() => {
    setTimeout(() => {
      animate();
    }, 2000);
    setTimeout(() => {
      animate();
    }, 3000);
  }, []);
  return (
    <>
      <ReanimatedArcBase
        color="coral"
        diameter={200}
        width={20}
        arcSweepAngle={arcAngle.current}
        lineCap="round"
        rotation={Reanimated.divide(arcAngle.current, 2)}
      />
      <View style={{paddingTop: 20}}>
        <Button title="Animate Arc!" onPress={animate} />
      </View>
    </>
  );
}
Example #6
Source File: AnimatedHelper.ts    From curved-bottom-navigation-bar with MIT License 6 votes vote down vote up
useSharedTransition = (
  state: boolean,
  config: WithTimingConfig = {
    duration: 500,
    easing: Easing.bezier(0.33, 0.01, 0, 1),
  }
): Animated.SharedValue<number> => {
  'worklet';
  return useDerivedValue(() =>
    state ? withTiming(1, config) : withTiming(0, config)
  );
}
Example #7
Source File: AnimatedHelper.ts    From curved-bottom-navigation-bar with MIT License 6 votes vote down vote up
sharedTiming = (
  toValue: number,
  config?: WithTimingConfig,
  callBack?: AnimationCallback
) => {
  'worklet';
  return withTiming(
    toValue,
    Object.assign(
      {
        duration: 500,
        easing: Easing.bezier(0.22, 1, 0.36, 1),
      },
      config
    ),
    callBack
  );
}
Example #8
Source File: CountdownTimer.tsx    From tic-tac-toe-app with MIT License 6 votes vote down vote up
CountdownTimer: React.FC<PropTypes> = ({ size, duration }) => {
    const clock = new Clock();

    const progress = timing({
        clock,
        duration,
        from: 0,
        to: 1,
        easing: Easing.linear,
    });

    return <CircularProgress size={size} progress={progress} />;
}
Example #9
Source File: Progress.tsx    From reanimated-arc with MIT License 5 votes vote down vote up
Progress = () => {
  const arcAngle = useRef(new Reanimated.Value(Math.random() * 240));
  const [text, setText] = useState('0%');
  const randomizeProgress = useCallback(() => {
    Reanimated.timing(arcAngle.current, {
      toValue: Math.random() * 240,
      easing: Easing.inOut(Easing.quad),
      duration: 1000,
    }).start();
  }, []);

  return (
    <View>
      <View style={styles.container}>
        <Reanimated.Code
          exec={Reanimated.call([arcAngle.current], ([value]) => {
            setText(`${Math.round((value / 240) * 100)}%`);
          })}
        />
        <ReanimatedArcBase
          color="lightgrey"
          diameter={200}
          width={20}
          arcSweepAngle={240}
          lineCap="round"
          rotation={240}
          style={styles.absolute}
        />
        <ReanimatedArcBase
          color="purple"
          diameter={200}
          width={20}
          arcSweepAngle={arcAngle.current}
          lineCap="round"
          rotation={240}
          style={styles.absolute}
        />
        <Text style={styles.text}>{text}</Text>
      </View>
      <Button title="Randomize progress" onPress={randomizeProgress} />
    </View>
  );
}
Example #10
Source File: ScalableImage.tsx    From react-native-gallery-toolkit with MIT License 5 votes vote down vote up
defaultTimingConfig = {
  duration: 250,
  easing: Easing.bezier(0.33, 0.01, 0, 1),
}
Example #11
Source File: ImageTransformer.tsx    From react-native-gallery-toolkit with MIT License 5 votes vote down vote up
defaultTimingConfig = {
  duration: 250,
  easing: Easing.bezier(0.33, 0.01, 0, 1),
}
Example #12
Source File: WeekIntro.tsx    From nyxo-app with GNU General Public License v3.0 5 votes vote down vote up
DEFAULT_EASING: Animated.EasingFunction = Easing.bezier(
  0.5,
  0,
  0.25,
  1
)
Example #13
Source File: Notification.tsx    From react-native-crypto-wallet-app with MIT License 5 votes vote down vote up
Notification: React.FC<INotification> = ({ type, message }) => {
  const opacity = useValue(0);
  const { dispatch } = useContext(AppContext);
  const { timing } = Animated;

  const animConfig = {
    duration: 300,
    easing: Easing.inOut(Easing.ease),
  };

  useEffect(() => {
    timing(opacity, { ...animConfig, toValue: 1 }).start();
  }, [animConfig, opacity, timing]);

  useEffect(() => {
    const fade = setTimeout(() => {
      timing(opacity, { ...animConfig, toValue: 0 }).start(({ finished }) => {
        if (finished) {
          dispatch({
            type: CLEAR_NOTIFICATION,
          });
        }
      });
    }, 2000);

    return () => {
      clearTimeout(fade);
    };
  }, [animConfig, dispatch, opacity, timing]);

  return (
    <SafeAreaView
      style={{
        ...StyleSheet.absoluteFillObject,
      }}
    >
      <AnimatedBox
        flexDirection="row"
        justifyContent="space-between"
        alignItems="center"
        height={56}
        position="absolute"
        top={16}
        left={16}
        right={16}
        backgroundColor="toast"
        borderRadius="full"
        {...{ opacity }}
        style={NotificationStyle.container}
      >
        <Box flexDirection="row" alignItems="center">
          <Icon name={type!} />
          <StyledText variant="label" color="white" style={NotificationStyle.message}>
            {message}
          </StyledText>
        </Box>
        <Icon name="x" color="white" />
      </AnimatedBox>
    </SafeAreaView>
  );
}
Example #14
Source File: index.tsx    From react-native-checkbox-reanimated with MIT License 5 votes vote down vote up
AnimatedCheckbox = (props: Props) => {
  const { checked, checkmarkColor, highlightColor, boxOutlineColor } = props

  const progress = useSharedValue(0)

  useEffect(() => {
    progress.value = withTiming(checked ? 1 : 0, {
      duration: checked ? 300 : 100,
      easing: Easing.linear
    })
  }, [checked])

  const animatedBoxProps = useAnimatedProps(
    () => ({
      stroke: interpolateColor(
        Easing.bezierFn(0.16, 1, 0.3, 1)(progress.value),
        [0, 1],
        [boxOutlineColor, highlightColor],
        'RGB'
      ),
      fill: interpolateColor(
        Easing.bezierFn(0.16, 1, 0.3, 1)(progress.value),
        [0, 1],
        ['#00000000', highlightColor],
        'RGB'
      )
    }),
    [highlightColor, boxOutlineColor]
  )

  return (
    <Svg
      viewBox={[-MARGIN, -MARGIN, vWidth + MARGIN, vHeight + MARGIN].join(' ')}
    >
      <Defs>
        <ClipPath id="clipPath">
          <Path
            fill="white"
            stroke="gray"
            strokeLinejoin="round"
            strokeLinecap="round"
            d={outlineBoxPath}
          />
        </ClipPath>
      </Defs>
      <AnimatedStroke
        progress={progress}
        d={checkMarkPath}
        stroke={highlightColor}
        strokeWidth={10}
        strokeLinejoin="round"
        strokeLinecap="round"
        strokeOpacity={checked || false ? 1 : 0}
      />
      <AnimatedPath
        d={outlineBoxPath}
        strokeWidth={7}
        strokeLinejoin="round"
        strokeLinecap="round"
        animatedProps={animatedBoxProps}
      />
      <G clipPath="url(#clipPath)">
        <AnimatedStroke
          progress={progress}
          d={checkMarkPath}
          stroke={checkmarkColor}
          strokeWidth={10}
          strokeLinejoin="round"
          strokeLinecap="round"
          strokeOpacity={checked || false ? 1 : 0}
        />
      </G>
    </Svg>
  )
}
Example #15
Source File: ReanimatedArc.tsx    From reanimated-arc with MIT License 5 votes vote down vote up
static defaultProps = {
    ...defaultProps,
    initialAnimation: true,
    animationDuration: 800,
    easing: Easing.linear,
  };
Example #16
Source File: Stopwatch.tsx    From reanimated-arc with MIT License 5 votes vote down vote up
easing = Easing.inOut(Easing.quad)
Example #17
Source File: Donut2.tsx    From reanimated-arc with MIT License 5 votes vote down vote up
easing = Easing.inOut(Easing.quad)
Example #18
Source File: Logo.tsx    From reanimated-arc with MIT License 5 votes vote down vote up
App = () => {
  const arcAngle = useRef(new Reanimated.Value(-90));

  const animate = () =>
    Reanimated.timing(arcAngle.current, {
      toValue: 270,
      easing: Easing.inOut(Easing.quad),
      duration: 2000,
    }).start();

  useEffect(() => {
    animate();
  }, []);

  return (
    <View style={styles.wrapper}>
      <ReanimatedArcBase
        color="#121330"
        diameter={200}
        width={20}
        lineCap="round"
        arcSweepAngle={160}
        rotation={Reanimated.add(arcAngle.current, 10)}
        style={styles.arc1}
      />
      <ReanimatedArcBase
        color="#3eefd8"
        diameter={140}
        width={20}
        lineCap="round"
        arcSweepAngle={170}
        rotation={Reanimated.multiply(
          Reanimated.sub(arcAngle.current, 185),
          -1,
        )}
        style={styles.arc2}
      />
      <ReanimatedArcBase
        color="#121330"
        diameter={80}
        width={20}
        lineCap="round"
        arcSweepAngle={180.1}
        rotation={arcAngle.current}
        style={styles.arc3}
      />
    </View>
  );
}
Example #19
Source File: AuthForm.tsx    From react-native-crypto-wallet-app with MIT License 4 votes vote down vote up
AuthForm: React.FC<IAuthFormProps> = ({
  isSignUp,
  email,
  isEmailValid,
  password,
  isPasswordValid,
  loading,
  isButtonDisabled,
  showPassword,
  submitButtonLabel,
  bottomSectionLightTextLabel,
  bottomSectionAccentTextLabel,
  onEmailChange,
  onPasswordChange,
  onShowPasswordPress,
  onSignUpPress,
  onForgotPasswordPress,
  onNavigationToLoginOrSignUp,
}) => {
  const keyboardDidShow = useKeyboardDidShow();
  const { height } = Dimensions.get('window');
  const UNFOCUSED_HEIGHT = (height * 57) / 100;
  const FOCUSED_HEIGHT = (height * 87) / 100;
  const containerInitialHeight = useValue(UNFOCUSED_HEIGHT);

  const containerAnimConfig = {
    duration: 200,
    easing: Easing.inOut(Easing.ease),
  };

  useEffect(() => {
    if (keyboardDidShow) {
      timing(containerInitialHeight, { ...containerAnimConfig, toValue: FOCUSED_HEIGHT }).start();
    } else {
      timing(containerInitialHeight, { ...containerAnimConfig, toValue: UNFOCUSED_HEIGHT }).start();
    }
  }, [
    FOCUSED_HEIGHT,
    UNFOCUSED_HEIGHT,
    containerAnimConfig,
    containerInitialHeight,
    keyboardDidShow,
  ]);

  return (
    <ContentContainer height={containerInitialHeight}>
      <StyledInput
        label="Email Address"
        value={email}
        onChangeText={onEmailChange}
        keyboardType="email-address"
        disabled={loading}
        errorText={isEmailValid === undefined || isEmailValid ? '' : 'Email address is not valid'}
        ariaLabel="email"
      />
      <StyledInput
        {...{ showPassword }}
        label="Password"
        value={password}
        onChangeText={onPasswordChange}
        onShowPasswordPress={onShowPasswordPress}
        disabled={loading}
        errorText={isPasswordValid === undefined || isPasswordValid ? '' : 'Password is not valid'}
        isPassword
        ariaLabel="password"
      />
      {!isSignUp && (
        <Box alignSelf="flex-end">
          <PressableText
            variant="link"
            label="Forgot your password?"
            onPress={onForgotPasswordPress!}
          />
        </Box>
      )}
      <View style={AuthFormStyle.bottomSection}>
        <BottomSection
          mainButtonVariant="primary"
          mainButtonLabel={submitButtonLabel}
          mainButtonLoading={loading}
          mainButtonDisabled={isButtonDisabled}
          lightTextLabel={bottomSectionLightTextLabel}
          accentTextLabel={bottomSectionAccentTextLabel}
          onMainButtonPress={onSignUpPress}
          onAccentTextPress={onNavigationToLoginOrSignUp}
        />
      </View>
    </ContentContainer>
  );
}
Example #20
Source File: Dot.tsx    From react-native-wagmi-charts with MIT License 4 votes vote down vote up
export function LineChartDot({
  at,
  color: defaultColor = 'black',
  dotProps,
  hasOuterDot: defaultHasOuterDot = false,
  hasPulse = false,
  inactiveColor,
  outerDotProps,
  pulseBehaviour = 'while-inactive',
  pulseDurationMs = 800,
  showInactiveColor = true,
  size = 4,
  outerSize = size * 4,
}: LineChartDotProps) {
  const { data, isActive } = useLineChart();
  const { path, pathWidth: width } = React.useContext(
    LineChartDimensionsContext
  );

  ////////////////////////////////////////////////////////////

  const { isInactive: _isInactive } = React.useContext(LineChartPathContext);
  const isInactive = showInactiveColor && _isInactive;
  const color = isInactive ? inactiveColor || defaultColor : defaultColor;
  const opacity = isInactive && !inactiveColor ? 0.5 : 1;
  const hasOuterDot = defaultHasOuterDot || hasPulse;

  ////////////////////////////////////////////////////////////

  const parsedPath = React.useMemo(() => parse(path), [path]);

  ////////////////////////////////////////////////////////////

  const pointWidth = React.useMemo(
    () => width / (data.length - 1),
    [data.length, width]
  );

  ////////////////////////////////////////////////////////////

  const x = useDerivedValue(() => withTiming(pointWidth * at));
  const y = useDerivedValue(() =>
    withTiming(getYForX(parsedPath!, x.value) || 0)
  );

  ////////////////////////////////////////////////////////////

  const animatedDotProps = useAnimatedProps(() => ({
    cx: x.value,
    cy: y.value,
  }));

  const animatedOuterDotProps = useAnimatedProps(() => {
    let defaultProps = {
      cx: x.value,
      cy: y.value,
      opacity: 0.1,
      r: outerSize,
    };

    if (!hasPulse) {
      return defaultProps;
    }

    if (isActive.value && pulseBehaviour === 'while-inactive') {
      return {
        ...defaultProps,
        r: 0,
      };
    }

    const easing = Easing.out(Easing.sin);
    const animatedOpacity = withRepeat(
      withSequence(
        withTiming(0.8),
        withTiming(0, {
          duration: pulseDurationMs,
          easing,
        })
      ),
      -1,
      false
    );
    const scale = withRepeat(
      withSequence(
        withTiming(0),
        withTiming(outerSize, {
          duration: pulseDurationMs,
          easing,
        })
      ),
      -1,
      false
    );

    if (pulseBehaviour === 'while-inactive') {
      return {
        ...defaultProps,
        opacity: isActive.value ? withTiming(0) : animatedOpacity,
        r: isActive.value ? withTiming(0) : scale,
      };
    }
    return {
      ...defaultProps,
      opacity: animatedOpacity,
      r: scale,
    };
  }, [outerSize]);

  ////////////////////////////////////////////////////////////

  return (
    <>
      <AnimatedCircle
        animatedProps={animatedDotProps}
        r={size}
        fill={color}
        opacity={opacity}
        {...dotProps}
      />
      {hasOuterDot && (
        <AnimatedCircle
          animatedProps={animatedOuterDotProps}
          fill={color}
          {...outerDotProps}
        />
      )}
    </>
  );
}
Example #21
Source File: ProgressBar.tsx    From jellyfin-audio-player with MIT License 4 votes vote down vote up
function ProgressBar() {
    const { position, buffered, duration } = useProgress();

    const width = useSharedValue(0);
    const pos = useSharedValue(0);
    const buf = useSharedValue(0);
    const dur = useSharedValue(0);

    const isDragging = useSharedValue(false);
    const offset = useSharedValue(0);

    const bufferAnimation = useDerivedValue(() => {
        return calculateProgressTranslation(buf.value, dur.value, width.value);
    }, [[dur, buf, width.value]]);

    const progressAnimation = useDerivedValue(() => {
        if (isDragging.value) {
            return calculateProgressTranslation(offset.value, width.value, width.value);
        } else {
            return calculateProgressTranslation(pos.value, dur.value, width.value);
        }
    });

    const timePassed = useDerivedValue(() => {
        if (isDragging.value) {
            const currentPosition = (offset.value - DRAG_HANDLE_SIZE / 2) / (width.value - DRAG_HANDLE_SIZE) * dur.value;
            return getMinutes(currentPosition) + ':' + getSeconds(currentPosition);
        } else {
            return getMinutes(pos.value) + ':' + getSeconds(pos.value);
        }
    }, [pos]);

    const timeRemaining = useDerivedValue(() => {
        if (isDragging.value) {
            const currentPosition = (offset.value - DRAG_HANDLE_SIZE / 2) / (width.value - DRAG_HANDLE_SIZE) * dur.value;
            const remaining = (currentPosition - dur.value) * -1;
            return `-${getMinutes(remaining)}:${getSeconds((remaining))}`;
        } else {
            const remaining = (pos.value - dur.value) * -1;
            return `-${getMinutes(remaining)}:${getSeconds((remaining))}`;
        }
    }, [pos, dur]);
    
    const pan = Gesture.Pan()
        .minDistance(1)
        .activeOffsetX(1)
        .activeOffsetY(1)
        .onBegin((e) => {
            isDragging.value = true;
            offset.value = Math.min(Math.max(DRAG_HANDLE_SIZE / 2, e.x), width.value - DRAG_HANDLE_SIZE / 2);
        }).onUpdate((e) => {
            offset.value = Math.min(Math.max(DRAG_HANDLE_SIZE / 2, e.x), width.value - DRAG_HANDLE_SIZE / 2);
        }).onFinalize(() => {
            pos.value = (offset.value - DRAG_HANDLE_SIZE / 2) / (width.value - DRAG_HANDLE_SIZE) * dur.value;
            isDragging.value = false;
            runOnJS(TrackPlayer.seekTo)(pos.value);
        });
    const tap = Gesture.Tap()
        .onBegin((e) => {
            isDragging.value = true;
            offset.value = Math.min(Math.max(DRAG_HANDLE_SIZE / 2, e.x), width.value - DRAG_HANDLE_SIZE / 2);
        }).onFinalize(() => {
            pos.value = (offset.value - DRAG_HANDLE_SIZE / 2) / (width.value - DRAG_HANDLE_SIZE) * dur.value;
            isDragging.value = false;
            runOnJS(TrackPlayer.seekTo)(pos.value);
        });
    const gesture = Gesture.Exclusive(pan, tap);

    useEffect(() => {
        pos.value = position;
        buf.value = buffered;
        dur.value = duration;
    // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [position, buffered, duration]);

    const dragHandleStyles = useAnimatedStyle(() => {
        return {
            transform: [
                { translateX: offset.value },
                { 
                    scale: withTiming(isDragging.value ? 1 : 0, {
                        duration: 100,
                        easing: Easing.out(Easing.ease),
                    })
                }
            ],
        };
    });

    const bufferStyles = useAnimatedStyle(() => ({
        transform: [
            { translateX: bufferAnimation.value }
        ]
    }));

    const progressStyles = useAnimatedStyle(() => {
        return {
            transform: [
                { translateX: progressAnimation.value }
            ]
        };
    });

    const timePassedStyles = useAnimatedStyle(() => {
        return {
            transform: [
                { translateY: withTiming(isDragging.value && offset.value < 48 ? 12 : 0, {
                    duration: 145,
                    easing: Easing.ease
                }) },
            ],
        };
    });

    const timeRemainingStyles = useAnimatedStyle(() => {
        return {
            transform: [
                { translateY: withTiming(isDragging.value && offset.value > width.value - 48 ? 12 : 0, {
                    duration: 150,
                    easing: Easing.ease
                }) },
            ],
        };
    });

    return (
        <GestureDetector gesture={gesture}>
            <Container onLayout={(e) => { width.value = e.nativeEvent.layout.width; }}>
                <ProgressTrackContainer>
                    <ProgressTrack
                        opacity={0.15}
                    />
                    <ProgressTrack
                        style={bufferStyles}
                        opacity={0.15}
                    />
                    <ProgressTrack
                        style={progressStyles}
                    />
                </ProgressTrackContainer>
                <DragHandle style={dragHandleStyles} />
                <NumberBar style={{ flex: 1 }}>
                    <Number text={timePassed} style={timePassedStyles} />
                    <Number text={timeRemaining} style={timeRemainingStyles} />
                </NumberBar>
            </Container>
        </GestureDetector>
    );
}
Example #22
Source File: SectionListExample.tsx    From react-native-scroll-bottom-sheet with MIT License 4 votes vote down vote up
SectionListExample: React.FC<Props> = () => {
  const snapPointsFromTop = [96, '45%', windowHeight - 264];
  const animatedPosition = React.useRef(new Value(0.5));
  const handleLeftRotate = concat(
    interpolate(animatedPosition.current, {
      inputRange: [0, 0.4, 1],
      outputRange: [25, 0, 0],
      extrapolate: Extrapolate.CLAMP,
    }),
    'deg'
  );
  const handleRightRotate = concat(
    interpolate(animatedPosition.current, {
      inputRange: [0, 0.4, 1],
      outputRange: [-25, 0, 0],
      extrapolate: Extrapolate.CLAMP,
    }),
    'deg'
  );
  const cardScale = interpolate(animatedPosition.current, {
    inputRange: [0, 0.6, 1],
    outputRange: [1, 1, 0.9],
    extrapolate: Extrapolate.CLAMP,
  });

  const renderSectionHeader = React.useCallback(
    ({ section }) => (
      <View style={styles.section}>
        <Text>{section.title}</Text>
      </View>
    ),
    []
  );

  const renderItem = React.useCallback(
    ({ item }) => <Transaction {...item} />,
    []
  );

  return (
    <View style={styles.container}>
      <View style={styles.balanceContainer}>
        <Text style={styles.poundSign}>£</Text>
        <Text style={styles.balance}>4,345</Text>
      </View>
      <ProgressBar
        style={styles.progressBar}
        progress={0.8}
        color={Colors.green600}
      />
      <Animated.Image
        source={require('../assets/card-front.png')}
        style={[styles.card, { transform: [{ scale: cardScale }] }]}
      />
      <View style={styles.row}>
        <View>
          <View style={styles.action}>
            <FontAwesome5 name="credit-card" size={24} color="black" />
          </View>
          <Text style={{ textAlign: 'center' }}>Account</Text>
        </View>
        <View>
          <View style={styles.action}>
            <FontAwesome5 name="eye" size={24} color="black" />
          </View>
          <Text style={{ textAlign: 'center' }}>Pin</Text>
        </View>
        <View>
          <View style={styles.action}>
            <Ionicons name="md-snow" size={24} color="black" />
          </View>
          <Text style={{ textAlign: 'center' }}>Freeze</Text>
        </View>
        <View>
          <View style={styles.action}>
            <FontAwesome5 name="plus" size={24} color="black" />
          </View>
          <Text style={{ textAlign: 'center' }}>Top up</Text>
        </View>
      </View>
      <ScrollBottomSheet<ListItemData>
        enableOverScroll
        removeClippedSubviews={Platform.OS === 'android' && sections.length > 0}
        componentType="SectionList"
        topInset={statusBarHeight + navBarHeight}
        animatedPosition={animatedPosition.current}
        snapPoints={snapPointsFromTop}
        initialSnapIndex={1}
        animationConfig={{
          easing: Easing.inOut(Easing.linear),
        }}
        renderHandle={() => (
          <Handle style={{ paddingVertical: 20, backgroundColor: '#F3F4F9' }}>
            <Animated.View
              style={[
                styles.handle,
                {
                  left: windowWidth / 2 - 20,
                  transform: [{ rotate: handleLeftRotate }],
                },
              ]}
            />
            <Animated.View
              style={[
                styles.handle,
                {
                  right: windowWidth / 2 - 20,
                  transform: [{ rotate: handleRightRotate }],
                },
              ]}
            />
          </Handle>
        )}
        contentContainerStyle={styles.contentContainerStyle}
        stickySectionHeadersEnabled
        sections={sections}
        keyExtractor={i => i.id}
        renderSectionHeader={renderSectionHeader}
        renderItem={renderItem}
      />
    </View>
  );
}