Skip to content
pixelfor Expo
Ctrl K

Input

Draw Signature

A signing pill opens into a drawing pad, with saved strokes you can edit

Try it on your phone

Draw Signature

Scan to open Draw Signature in Expo Go

Install Expo Go first, then scan this QR to run the native component.

GET EXPO GO ONGoogle Play

This demo uses Expo SDK 54. If your installed Expo Go supports a different SDK, get the compatible build. iOS needs a build supporting this SDK.

Expo code

Original web component

Watermelon UI

Our Expo component

React Native Web

Component source

Make it yours

One file to copy into your app.

draw-signature.tsx

import React, { useEffect, useLayoutEffect, useRef, useState } from "react"
import {
  Platform,
  Pressable,
  StyleSheet,
  Text,
  View,
  useColorScheme,
  useWindowDimensions,
  type StyleProp,
  type ViewStyle,
} from "react-native"
import Animated, {
  Easing,
  ReduceMotion,
  interpolateColor,
  useAnimatedProps,
  useAnimatedStyle,
  useSharedValue,
  withTiming,
  type SharedValue,
} from "react-native-reanimated"
import { Gesture, GestureDetector } from "react-native-gesture-handler"
import { runOnUI, scheduleOnRN } from "react-native-worklets"
import Svg, { Path } from "react-native-svg"
const ICONS = {
  draw: {
    viewBox: "0 0 24 24",
    paths: [
      "m18.85 10.39 1.06-1.06c.78-.78.78-2.05 0-2.83L18.5 5.09c-.78-.78-2.05-.78-2.83 0l-1.06 1.06zm-5.66-2.83L4 16.76V21h4.24l9.19-9.19zM19 17.5c0 2.19-2.54 3.5-5 3.5-.55 0-1-.45-1-1s.45-1 1-1c1.54 0 3-.73 3-1.5 0-.47-.48-.87-1.23-1.2l1.48-1.48c1.07.63 1.75 1.47 1.75 2.68M4.58 13.35C3.61 12.79 3 12.06 3 11c0-1.8 1.89-2.63 3.56-3.36C7.59 7.18 9 6.56 9 6c0-.41-.78-1-2-1-1.26 0-1.8.61-1.83.64-.35.41-.98.46-1.4.12a.99.99 0 0 1-.15-1.38C3.73 4.24 4.76 3 7 3s4 1.32 4 3c0 1.87-1.93 2.72-3.64 3.47C6.42 9.88 5 10.5 5 11c0 .31.43.6 1.07.86z",
    ],
  },
  check: {
    viewBox: "0 0 512 512",
    paths: [
      "M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z",
    ],
  },
  redo: {
    viewBox: "0 0 512 512",
    paths: [
      "M500.33 0h-47.41a12 12 0 0 0-12 12.57l4 82.76A247.42 247.42 0 0 0 256 8C119.34 8 7.9 119.53 8 256.19 8.1 393.07 119.1 504 256 504a247.1 247.1 0 0 0 166.18-63.91 12 12 0 0 0 .48-17.43l-34-34a12 12 0 0 0-16.38-.55A176 176 0 1 1 402.1 157.8l-101.53-4.87a12 12 0 0 0-12.57 12v47.41a12 12 0 0 0 12 12h200.33a12 12 0 0 0 12-12V12a12 12 0 0 0-12-12z",
    ],
  },
  pen: {
    viewBox: "0 0 512 512",
    paths: [
      "M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z",
    ],
  },
} as const
export type SignatureStep = "idle" | "drawing" | "done"
export type SignatureData = {
  path: string
  width: number
  height: number
  svg: string
}
export type DrawSignatureColors = {
  pad: string
  button: string
  buttonHover: string
  label: string
  done: string
  doneLabel: string
  ink: string
  border: string
  header: string
  clear: string
  clearHover: string
  close: string
  closeHover: string
}
export type DrawSignatureComponentProps = {
  startLabel?: string
  finishLabel?: string
  doneLabel?: string
  defaultStep?: SignatureStep
  onFinish?: (signature: SignatureData | null) => void
  onClear?: () => void
  onStepChange?: (step: SignatureStep) => void
  theme?: "light" | "dark" | "system"
  colors?: Partial<DrawSignatureColors>
  width?: number
  canvasWidth?: number
  canvasHeight?: number
  radius?: number
  fontSize?: number
  strokeWidth?: number
  style?: StyleProp<ViewStyle>
  testID?: string
}
const WEB = Platform.OS === "web",
  FONT = WEB ? "Arial" : Platform.OS === "ios" ? "Helvetica" : "sans-serif"
const LIGHT: DrawSignatureColors = {
  pad: "#fff",
  button: "#f5f5f5",
  buttonHover: "#e5e5e5",
  label: "#171717",
  done: "#171717",
  doneLabel: "#fff",
  ink: "#000",
  border: "#d4d4d4",
  header: "#737373",
  clear: "#a3a3a3",
  clearHover: "#525252",
  close: "#a3a3a3",
  closeHover: "#737373",
}
const DARK: DrawSignatureColors = {
  pad: "#171717",
  button: "#262626",
  buttonHover: "#404040",
  label: "#f5f5f5",
  done: "#f5f5f5",
  doneLabel: "#171717",
  ink: "#fff",
  border: "#404040",
  header: "#a3a3a3",
  clear: "#737373",
  clearHover: "#d4d4d4",
  close: "#404040",
  closeHover: "#525252",
}
function spring(t: number) {
  "worklet"
  const seconds = t * 0.7,
    decay = 21.103438635377675 / 2,
    angular = Math.sqrt(154.1021184211596 - decay * decay)
  return (
    1 -
    Math.exp(-decay * seconds) *
      (Math.cos(angular * seconds) +
        (decay / angular) * Math.sin(angular * seconds))
  )
}
const MORPH = {
  duration: 700,
  easing: spring,
  reduceMotion: ReduceMotion.System,
} as const
const BORDER = {
  duration: 400,
  easing: Easing.bezier(0, 0, 0.2, 1),
  reduceMotion: ReduceMotion.System,
} as const
const limit = (x: number) => {
  "worklet"
  return Math.min(1, Math.max(0, x))
}
function entering(x: number) {
  "worklet"
  const t = limit(x * 2)
  return Math.sqrt(1 - (t - 1) * (t - 1))
}
function leaving(x: number) {
  "worklet"
  return 1 - limit((x - 0.5) / 0.45)
}
const AnimatedPath = Animated.createAnimatedComponent(Path)
function Icon({
  name,
  size,
  color,
}: {
  name: keyof typeof ICONS | "close"
  size: number
  color: string
}) {
  if (name === "close")
    return (
      <Svg width={size} height={size} viewBox="0 0 24 24">
        <Path
          d="M18 6 6 18M6 6l12 12"
          stroke={color}
          strokeWidth={2}
          strokeLinecap="round"
        />
      </Svg>
    )
  const data = ICONS[name]
  return (
    <Svg width={size} height={size} viewBox={data.viewBox}>
      {data.paths.map((d, i) => (
        <Path key={i} d={d} fill={color} />
      ))}
    </Svg>
  )
}
function Drawing({
  path,
  width,
  height,
  canvasWidth,
  strokeWidth,
  ink,
  testID,
}: {
  path: SharedValue<string>
  width: number
  height: number
  canvasWidth: number
  strokeWidth: number
  ink: string
  testID?: string
}) {
  const hasPoint = useSharedValue(false)
  const downX = useSharedValue(0),
    downY = useSharedValue(0)
  const gesture = Gesture.Pan()
    .minDistance(0)
    .maxPointers(1)
    .shouldCancelWhenOutside(true)
    .onBegin(({ x, y }) => {
      hasPoint.value = false
      downX.value = Math.round(x * 100) / 100
      downY.value = Math.round(y * 100) / 100
    })
    .onUpdate(({ x, y }) => {
      const px = Math.round(x * 100) / 100,
        py = Math.round(y * 100) / 100
      if (!hasPoint.value && px === downX.value && py === downY.value) return
      path.value += `${hasPoint.value ? "L" : "M"}${px} ${py} `
      hasPoint.value = true
    })
    .onFinalize(() => {
      hasPoint.value = false
    })
  const props = useAnimatedProps(() => ({ d: path.value }))
  return (
    <GestureDetector gesture={gesture}>
      <View
        testID={testID}
        accessibilityLabel="Signature drawing area"
        style={
          {
            width,
            height,
            overflow: "hidden",
            ...(WEB
              ? {
                  touchAction: "none",
                  cursor: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="34" height="34" viewBox="0 0 24 24" fill="${ink === "#fff" ? "white" : "black"}" stroke="${ink === "#fff" ? "black" : "white"}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>') 0 22, auto`,
                }
              : {}),
          } as ViewStyle
        }
      >
        <Svg
          width={width}
          height={height}
          viewBox={`0 0 ${canvasWidth} ${height}`}
          preserveAspectRatio="none"
          pointerEvents="none"
        >
          <AnimatedPath
            animatedProps={props}
            fill="none"
            stroke={ink}
            strokeWidth={strokeWidth}
            strokeLinecap="round"
            strokeLinejoin="round"
          />
        </Svg>
      </View>
    </GestureDetector>
  )
}
function SmallButton({
  name,
  size,
  background,
  hoverBackground,
  color,
  hoverColor,
  onPress,
  label,
}: {
  name: "redo" | "close" | "pen"
  size: number
  background?: string
  hoverBackground?: string
  color: string
  hoverColor?: string
  onPress: () => void
  label: string
}) {
  const [hover, setHover] = useState(false)
  return (
    <Pressable
      accessibilityRole="button"
      accessibilityLabel={label}
      hitSlop={name === "redo" ? 13 : name === "close" ? 10 : 0}
      onPress={onPress}
      onHoverIn={() => setHover(true)}
      onHoverOut={() => setHover(false)}
      style={{
        width: size,
        height: size,
        borderRadius: size / 2,
        alignItems: "center",
        justifyContent: "center",
        backgroundColor: hover ? (hoverBackground ?? background) : background,
      }}
    >
      <Icon
        name={name}
        size={name === "close" ? 20 : 22}
        color={hover ? (hoverColor ?? color) : color}
      />
    </Pressable>
  )
}
export function DrawSignatureComponent({
  startLabel = "Start Signing",
  finishLabel = "Finish Signing",
  doneLabel = "Signing Done",
  defaultStep = "idle",
  onFinish,
  onClear,
  onStepChange,
  theme = "system",
  colors: overrides,
  width = 320,
  canvasWidth = 290,
  canvasHeight = 200,
  radius = 32,
  fontSize = 18,
  strokeWidth = 3,
  style,
  testID,
}: DrawSignatureComponentProps) {
  const system = useColorScheme(),
    { fontScale } = useWindowDimensions()
  const colors = {
    ...(theme === "dark" || (theme === "system" && system === "dark")
      ? DARK
      : LIGHT),
    ...overrides,
  }
  const [step, setStep] = useState<SignatureStep>(defaultStep),
    [previous, setPrevious] = useState<SignatureStep | null>(null)
  const [padMounted, setPadMounted] = useState(defaultStep === "drawing"),
    [textWidths, setTextWidths] = useState({
      idle: 112,
      drawing: 124,
      done: 116,
    })
  const saved = useRef(""),
    finishPending = useRef(false),
    exitTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
  const path = useSharedValue(""),
    transition = useSharedValue(1),
    padExit = useSharedValue(0),
    border = useSharedValue(defaultStep === "drawing" ? 1 : 0),
    press = useSharedValue(1),
    hover = useSharedValue(0)
  const lineHeight = (28 / 18) * fontSize * fontScale,
    padHeight =
      24 +
      Math.max(28, lineHeight) +
      24 +
      canvasHeight +
      16 +
      Math.max(28, lineHeight) +
      32 +
      16
  const idleWidth = textWidths.idle + 24 + 8 + 64,
    donePillWidth = textWidths.done + 24 + 8 + 48,
    doneWidth = donePillWidth + 12 + 54
  const sizes = {
    idle: { w: idleWidth + 8, h: lineHeight + 48 },
    drawing: { w: width + 8, h: padHeight + 8 },
    done: { w: doneWidth + 8, h: Math.max(lineHeight + 32, 54) + 8 },
  }
  const initial = sizes[defaultStep]
  const rootW = useSharedValue(initial.w),
    rootH = useSharedValue(initial.h)
  const rectangles = {
    idle: {
      x: -idleWidth / 2,
      y: -(lineHeight + 40) / 2,
      w: idleWidth,
      h: lineHeight + 40,
      ix: -idleWidth / 2 + 32 + 12,
      iy: 0,
      is: 24,
      tx: -idleWidth / 2 + 64,
      ty: 0,
      tw: textWidths.idle,
    },
    drawing: {
      x: -(width - 48) / 2,
      y: padHeight / 2 - 16 - (lineHeight + 32),
      w: width - 48,
      h: lineHeight + 32,
      ix: -(textWidths.drawing + 36) / 2 + 14,
      iy: padHeight / 2 - 16 - (lineHeight + 32) / 2,
      is: 28,
      tx: -(textWidths.drawing + 36) / 2 + 36,
      ty: padHeight / 2 - 16 - (lineHeight + 32) / 2,
      tw: textWidths.drawing,
    },
    done: {
      x: -doneWidth / 2,
      y: -(lineHeight + 32) / 2,
      w: doneWidth,
      h: lineHeight + 32,
      ix: -doneWidth / 2 + 24 + 12,
      iy: 0,
      is: 24,
      tx: -doneWidth / 2 + 56,
      ty: 0,
      tw: textWidths.done,
    },
  }
  const r = rectangles[defaultStep]
  const bx = useSharedValue(r.x),
    by = useSharedValue(r.y),
    bw = useSharedValue(r.w),
    bh = useSharedValue(r.h)
  const ix = useSharedValue(r.ix),
    iy = useSharedValue(r.iy),
    iconSize = useSharedValue(r.is),
    tx = useSharedValue(r.tx),
    ty = useSharedValue(r.ty),
    tw = useSharedValue(r.tw)
  useEffect(() => {
    onStepChange?.(step)
  }, [step, onStepChange])
  useEffect(
    () => () => {
      if (exitTimer.current) clearTimeout(exitTimer.current)
    },
    []
  )
  useLayoutEffect(() => {
    const next = rectangles[step]
    bx.value = withTiming(next.x, MORPH)
    by.value = withTiming(next.y, MORPH)
    bw.value = withTiming(next.w, MORPH)
    bh.value = withTiming(next.h, MORPH)
    ix.value = withTiming(next.ix, MORPH)
    iy.value = withTiming(next.iy, MORPH)
    iconSize.value = withTiming(next.is, MORPH)
    tx.value = withTiming(next.tx, MORPH)
    ty.value = withTiming(next.ty, MORPH)
    tw.value = withTiming(next.tw, MORPH)
    border.value = withTiming(step === "drawing" ? 1 : 0, BORDER)
  }, [step, textWidths, fontScale, width, fontSize, padHeight])
  const moveTo = (next: SignatureStep) => {
    if (exitTimer.current) clearTimeout(exitTimer.current)
    setPrevious(step)
    transition.value = 0
    transition.value = withTiming(1, MORPH)
    if (next === "drawing") {
      path.value = saved.current
      setPadMounted(true)
      padExit.value = 0
    } else if (step === "drawing") {
      padExit.value = withTiming(1, MORPH)
      exitTimer.current = setTimeout(() => {
        setPadMounted(false)
        setPrevious(null)
      }, 700)
    } else {
      exitTimer.current = setTimeout(() => setPrevious(null), 700)
    }
    press.value = 1
    hover.value = 0
    setStep(next)
  }
  const clear = () => {
    path.value = ""
    saved.current = ""
    onClear?.()
  }
  const complete = (value: string) => {
    saved.current = value
    finishPending.current = false
    moveTo("done")
    const safeInk = colors.ink
      .replace(/&/g, "&amp;")
      .replace(/"/g, "&quot;")
      .replace(/</g, "&lt;")
    onFinish?.({
      path: value,
      width: canvasWidth,
      height: canvasHeight,
      svg: `<svg xmlns="http://www.w3.org/2000/svg" width="${canvasWidth}" height="${canvasHeight}" viewBox="0 0 ${canvasWidth} ${canvasHeight}"><path d="${value}" fill="none" stroke="${safeInk}" stroke-width="${strokeWidth}" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
    })
  }
  const finish = () => {
    if (finishPending.current) return
    finishPending.current = true
    runOnUI(() => {
      "worklet"
      scheduleOnRN(complete, path.value)
    })()
  }
  const rootStyle = useAnimatedStyle(() => ({
    width: rootW.value,
    height: rootH.value,
    borderColor: interpolateColor(
      border.value,
      [0, 1],
      ["transparent", colors.border]
    ),
  }))
  const padStyle = useAnimatedStyle(() => ({
    left: step === "drawing" ? (rootW.value - width) / 2 - 4 : 0,
    top: step === "drawing" ? (rootH.value - padHeight) / 2 - 4 : 0,
    opacity: 1 - limit(padExit.value),
    transform: [
      { translateX: -width * 0.1 * padExit.value },
      { translateY: -padHeight * 0.3 * padExit.value },
    ],
  }))
  const buttonStyle = useAnimatedStyle(() => ({
    left: rootW.value / 2 + bx.value - 4,
    top: rootH.value / 2 + by.value - 4,
    width: bw.value,
    height: bh.value,
    opacity: 1,
    transform: [{ scale: press.value }],
  }))
  const pillStyle = useAnimatedStyle(() => ({
    width: step === "done" ? (bw.value * donePillWidth) / doneWidth : bw.value,
    height: bh.value,
    borderRadius: bh.value / 2,
    backgroundColor:
      step === "done"
        ? colors.done
        : interpolateColor(
            hover.value,
            [0, 1],
            [colors.button, colors.buttonHover]
          ),
  }))
  const editStyle = useAnimatedStyle(() => ({
    left: (bw.value * (donePillWidth + 12)) / doneWidth,
    top: (bh.value - 54) / 2,
    width: 54,
    height: 54,
    transform: [{ scaleX: bw.value / doneWidth }],
  }))
  const iconStyle = useAnimatedStyle(() => ({
    left: rootW.value / 2 + ix.value - 18 - 4,
    top: rootH.value / 2 + iy.value - 18 - 4,
    transform: [
      {
        translateX: (ix.value - (bx.value + bw.value / 2)) * (press.value - 1),
      },
      {
        translateY: (iy.value - (by.value + bh.value / 2)) * (press.value - 1),
      },
      { scale: (iconSize.value / 36) * press.value },
    ],
  }))
  const textStyle = useAnimatedStyle(() => ({
    left: rootW.value / 2 + tx.value - 4,
    top: rootH.value / 2 + ty.value - lineHeight / 2 - 4,
    width: tw.value,
    height: lineHeight,
    transform: [
      {
        translateX: (tx.value - (bx.value + bw.value / 2)) * (press.value - 1),
      },
      {
        translateY: (ty.value - (by.value + bh.value / 2)) * (press.value - 1),
      },
      { scale: press.value },
    ],
  }))
  const enterStyle = useAnimatedStyle(() => ({
    opacity: previous ? entering(transition.value) : 1,
  }))
  const exitStyle = useAnimatedStyle(() => ({
    opacity: previous === "idle" ? leaving(transition.value) : 0,
  }))
  const labels = { idle: startLabel, drawing: finishLabel, done: doneLabel }
  const incomingText = useAnimatedStyle(() => ({
    transform: [{ scaleX: tw.value / textWidths[step] }],
  }))
  const outgoingText = useAnimatedStyle(() => ({
    transform: [
      { scaleX: tw.value / (previous ? textWidths[previous] : tw.value) },
    ],
  }))
  const textBase = {
    fontFamily: FONT,
    fontWeight: "700" as const,
    fontSize,
    lineHeight: (28 / 18) * fontSize,
    includeFontPadding: false,
  }
  return (
    <Animated.View
      testID={testID}
      style={[
        styles.root,
        { borderRadius: radius, alignSelf: "center" },
        style,
        rootStyle,
      ]}
    >
      <View
        pointerEvents="none"
        accessible={false}
        accessibilityElementsHidden
        importantForAccessibility="no-hide-descendants"
        style={{
          position: "absolute",
          width: sizes[step].w,
          height: sizes[step].h,
          opacity: 0,
        }}
        onLayout={() => {
          rootW.value = withTiming(sizes[step].w, MORPH)
          rootH.value = withTiming(sizes[step].h, MORPH)
        }}
      />
      <View
        pointerEvents="none"
        accessible={false}
        accessibilityElementsHidden
        importantForAccessibility="no-hide-descendants"
        style={{
          position: "absolute",
          width: 0,
          height: 0,
          overflow: "hidden",
        }}
      >
        <View style={{ width: 10000, alignItems: "flex-start" }}>
          {(["idle", "drawing", "done"] as const).map((k) => (
            <Text
              key={k}
              style={textBase}
              onLayout={(e) => {
                const measured = Math.round(e.nativeEvent.layout.width)
                if (measured > 0)
                  setTextWidths((old) =>
                    old[k] === measured ? old : { ...old, [k]: measured }
                  )
              }}
            >
              {labels[k]}
            </Text>
          ))}
        </View>
      </View>
      {padMounted && (
        <Animated.View
          pointerEvents={step === "drawing" ? "auto" : "none"}
          style={[
            {
              position: "absolute",
              width,
              height: padHeight,
              backgroundColor: colors.pad,
              borderRadius: 34,
              padding: 24,
              paddingBottom: 16,
            },
            padStyle,
          ]}
        >
          <View
            style={{
              height: Math.max(28, lineHeight),
              flexDirection: "row",
              alignItems: "center",
              justifyContent: "space-between",
              marginBottom: 24,
            }}
          >
            <SmallButton
              name="redo"
              size={22}
              color={colors.clear}
              hoverColor={colors.clearHover}
              onPress={clear}
              label="Clear signature"
            />
            <Text style={[textBase, { color: colors.header }]}>Sign</Text>
            <SmallButton
              name="close"
              size={28}
              background={colors.close}
              hoverBackground={colors.closeHover}
              color="#fff"
              onPress={() => moveTo("idle")}
              label="Cancel signing"
            />
          </View>
          <Drawing
            path={path}
            width={width - 48}
            height={canvasHeight}
            canvasWidth={canvasWidth}
            strokeWidth={strokeWidth}
            ink={colors.ink}
            testID={testID ? `${testID}-canvas` : undefined}
          />
        </Animated.View>
      )}
      <Animated.View style={[{ position: "absolute" }, buttonStyle]}>
        {previous === "idle" && (
          <Animated.View
            pointerEvents="none"
            style={[
              { position: "absolute", backgroundColor: colors.button },
              pillStyle,
              exitStyle,
            ]}
          />
        )}
        <Animated.View
          pointerEvents="none"
          style={[{ position: "absolute" }, pillStyle, enterStyle]}
        />
        {step === "done" ? (
          <Animated.View
            style={[{ position: "absolute" }, editStyle, enterStyle]}
          >
            <SmallButton
              name="pen"
              size={54}
              background={colors.button}
              hoverBackground={colors.buttonHover}
              color={colors.label}
              onPress={() => moveTo("drawing")}
              label="Edit signature"
            />
          </Animated.View>
        ) : (
          <Pressable
            style={{ width: "100%", height: "100%" }}
            accessibilityRole="button"
            accessibilityLabel={labels[step]}
            onPress={step === "idle" ? () => moveTo("drawing") : finish}
            onHoverIn={() => {
              hover.value = 1
            }}
            onHoverOut={() => {
              hover.value = 0
            }}
            onPressIn={() => {
              if (step === "drawing") press.value = withTiming(0.97, MORPH)
            }}
            onPressOut={() => {
              press.value = withTiming(1, MORPH)
            }}
          />
        )}
      </Animated.View>
      <Animated.View
        pointerEvents="none"
        style={[{ position: "absolute", width: 36, height: 36 }, iconStyle]}
      >
        {previous && (
          <Animated.View style={[StyleSheet.absoluteFill, exitStyle]}>
            <Icon
              name={previous === "done" ? "check" : "draw"}
              size={36}
              color={previous === "done" ? colors.doneLabel : colors.label}
            />
          </Animated.View>
        )}
        <Animated.View style={enterStyle}>
          <Icon
            name={step === "done" ? "check" : "draw"}
            size={36}
            color={step === "done" ? colors.doneLabel : colors.label}
          />
        </Animated.View>
      </Animated.View>
      <Animated.View
        pointerEvents="none"
        style={[
          {
            position: "absolute",
            overflow: "visible",
            transformOrigin: "left center",
          },
          textStyle,
        ]}
      >
        {previous && (
          <Animated.View
            style={[
              {
                position: "absolute",
                width: textWidths[previous],
                height: lineHeight,
                left: 0,
                transformOrigin: "left center",
              },
              exitStyle,
              outgoingText,
            ]}
          >
            <Text
              style={[
                textBase,
                {
                  color: previous === "done" ? colors.doneLabel : colors.label,
                },
              ]}
            >
              {labels[previous]}
            </Text>
          </Animated.View>
        )}
        <Animated.View
          style={[
            {
              width: textWidths[step],
              height: lineHeight,
              transformOrigin: "left center",
            },
            enterStyle,
            incomingText,
          ]}
        >
          <Text
            style={[
              textBase,
              { color: step === "done" ? colors.doneLabel : colors.label },
            ]}
          >
            {labels[step]}
          </Text>
        </Animated.View>
      </Animated.View>
    </Animated.View>
  )
}
const styles = StyleSheet.create({
  root: {
    position: "relative",
    overflow: "hidden",
    borderWidth: 4,
    borderStyle: "dashed",
  },
})

Give the gestures and motion a run on a real Android phone.

Back to Watermelon UI