Skip to content
pixelfor Expo
Ctrl K

Disclosure

Create New Disclosure

A pill opens into six choices while its label settles on a slower curve

Try it on your phone

Create New Disclosure

Scan to open Create New Disclosure 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.

create-new-disclosure.tsx

import React, { useEffect, useRef, useState } from "react"
import {
  Platform,
  Pressable,
  StyleSheet,
  Text,
  View,
  useColorScheme,
  useWindowDimensions,
  type StyleProp,
  type ViewStyle,
} from "react-native"
import Animated, {
  Easing,
  ReduceMotion,
  interpolateColor,
  useAnimatedStyle,
  useSharedValue,
  withSpring,
  withTiming,
} from "react-native-reanimated"
import Svg, { Path } from "react-native-svg"
const FONT =
  Platform.OS === "web"
    ? "Arial"
    : Platform.OS === "ios"
      ? "Helvetica"
      : "sans-serif"
const SPRING = {
  duration: 400,
  reduceMotion: ReduceMotion.System,
  easing: (p: number) => {
    "worklet"
    const t = p * 0.4,
      decay = 19.081900916837842,
      angular = 9.241786416350251
    return (
      1 -
      Math.exp(-decay * t) *
        (Math.cos(angular * t) + (decay / angular) * Math.sin(angular * t))
    )
  },
} as const
const EXIT = {
  duration: 100,
  easing: Easing.bezier(0.25, 0.1, 0.35, 1),
  reduceMotion: ReduceMotion.System,
} as const
const LABEL = {
  duration: 450,
  easing: Easing.bezier(0.4, 0, 0.1, 1),
  reduceMotion: ReduceMotion.System,
} as const
const HOVER = {
  duration: 200,
  easing: Easing.bezier(0.4, 0, 0.2, 1),
  reduceMotion: ReduceMotion.System,
} as const
const PRESS = {
  stiffness: 550,
  damping: 30,
  mass: 1,
  reduceMotion: ReduceMotion.System,
} as const
const ICON_PATHS: Record<string, string[]> = {
  plus: ["M12.001 5.00003V19.002", "M19.002 12.002L4.99998 12.002"],
  close: ["M18 6L6.00081 17.9992M17.9992 18L6 6.00085"],
  project: [
    "M8 7H16.75C18.8567 7 19.91 7 20.6667 7.50559C20.9943 7.72447 21.2755 8.00572 21.4944 8.33329C22 9.08996 22 10.1433 22 12.25C22 15.7612 22 17.5167 21.1573 18.7779C20.7926 19.3238 20.3238 19.7926 19.7779 20.1573C18.5167 21 16.7612 21 13.25 21H12C7.28595 21 4.92893 21 3.46447 19.5355C2 18.0711 2 15.714 2 11V7.94427C2 6.1278 2 5.21956 2.38032 4.53806C2.65142 4.05227 3.05227 3.65142 3.53806 3.38032C4.21956 3 5.1278 3 6.94427 3C8.10802 3 8.6899 3 9.19926 3.19101C10.3622 3.62712 10.8418 4.68358 11.3666 5.73313L12 7",
  ],
  task: [
    "M13.5 2H8.5C7.67157 2 7 2.67157 7 3.5C7 4.32843 7.67157 5 8.5 5H13.5C14.3284 5 15 4.32843 15 3.5C15 2.67157 14.3284 2 13.5 2Z",
    "M7 15H10.4286M7 11H15",
    "M19 11.9995L19 9.48263C19 6.65424 19 5.24004 18.1213 4.36137C17.48 3.72007 16.5535 3.54681 15 3.5M11 21.9995L9 21.9995C6.17158 21.9995 4.75737 21.9995 3.87869 21.1208C3.00001 20.2421 3.00001 18.8279 3 15.9995L3.00001 9.48269C3.00001 6.65425 3 5.24004 3.87868 4.36136C4.51997 3.72007 5.44651 3.54681 6.9999 3.5",
    "M15.7367 21.6527L14 22L14.3473 20.2633C14.4179 19.9106 14.5913 19.5866 14.8456 19.3323L18.9111 15.2668C19.2668 14.9111 19.8437 14.9111 20.1995 15.2668L20.7332 15.8005C21.0889 16.1563 21.0889 16.7332 20.7332 17.0889L16.6677 21.1544C16.4134 21.4087 16.0894 21.5821 15.7367 21.6527Z",
  ],
  note: [
    "M12.8809 7.01656L17.6538 8.28825M11.8578 10.8134L14.2442 11.4492M11.9765 17.9664L12.9311 18.2208C15.631 18.9401 16.981 19.2998 18.0445 18.6893C19.108 18.0787 19.4698 16.7363 20.1932 14.0516L21.2163 10.2548C21.9398 7.57005 22.3015 6.22768 21.6875 5.17016C21.0735 4.11264 19.7235 3.75295 17.0235 3.03358L16.0689 2.77924C13.369 2.05986 12.019 1.70018 10.9555 2.31074C9.89196 2.9213 9.53023 4.26367 8.80678 6.94841L7.78366 10.7452C7.0602 13.4299 6.69848 14.7723 7.3125 15.8298C7.92652 16.8874 9.27651 17.2471 11.9765 17.9664Z",
    "M12 20.9462L11.0477 21.2055C8.35403 21.939 7.00722 22.3057 5.94619 21.6832C4.88517 21.0607 4.52429 19.692 3.80253 16.9546L2.78182 13.0833C2.06006 10.3459 1.69918 8.97718 2.31177 7.89892C2.84167 6.96619 4 7.00015 5.5 7.00003",
  ],
  goal: [
    "M15.9349 21.033C16.1789 21.5331 15.699 22 15.1239 22H8.87606C8.30096 22 7.82107 21.5331 8.06509 21.033C8.63149 19.8722 10.1793 19 12 19C13.8207 19 15.3685 19.8722 15.9349 21.033Z",
    "M12 16L11.9883 19",
    "M17.9866 4.49139C17.992 4.30176 17.9963 4.11477 17.9997 3.93095C18.0194 2.86463 17.1162 2.00002 16.0093 2.00002L7.99052 2C6.88381 2 5.98074 2.86438 6.00031 3.93054C6.00358 4.10833 6.00768 4.28907 6.01283 4.47231C6.05734 6.05627 6.18024 7.82593 6.51875 9.47415C7.18043 12.6958 8.66606 16 12.0001 16C15.3341 16 16.8193 12.6958 17.481 9.47415C17.8181 7.83255 17.9415 6.07048 17.9866 4.49139Z",
    "M18 4.5L19.4447 4.68748C20.9062 4.87714 22 6.122 22 7.59578C22 8.99373 21.0133 10.1973 19.6425 10.4715L17.5 11",
    "M6 4.5L4.55527 4.68748C3.09375 4.87714 2 6.122 2 7.59578C2 8.99373 2.98673 10.1973 4.35754 10.4715L6.5 11",
  ],
  milestone: [
    "M4 7L4 21",
    "M11.7576 3.90865C8.45236 2.22497 5.85125 3.21144 4.55426 4.2192C4.32048 4.40085 4.20358 4.49167 4.10179 4.69967C4 4.90767 4 5.10138 4 5.4888V14.7319C4.9697 13.6342 7.87879 11.9328 11.7576 13.9086C15.224 15.6744 18.1741 14.9424 19.5697 14.1795C19.7633 14.0737 19.8601 14.0207 19.9301 13.9028C20 13.7849 20 13.6569 20 13.4009V5.87389C20 5.04538 20 4.63113 19.8027 4.48106C19.6053 4.33099 19.1436 4.459 18.2202 4.71504C16.64 5.15319 14.3423 5.22532 11.7576 3.90865Z",
  ],
  reminder: [
    "M16 2V6M8 2V6",
    "M13 4H11C7.22876 4 5.34315 4 4.17157 5.17157C3 6.34315 3 8.22876 3 12V14C3 17.7712 3 19.6569 4.17157 20.8284C5.34315 22 7.22876 22 11 22H13C16.7712 22 18.6569 22 19.8284 20.8284C21 19.6569 21 17.7712 21 14V12C21 8.22876 21 6.34315 19.8284 5.17157C18.6569 4 16.7712 4 13 4Z",
    "M3 10H21",
  ],
}
type IconName =
  | "plus"
  | "close"
  | "project"
  | "task"
  | "note"
  | "goal"
  | "milestone"
  | "reminder"
function Icon({
  name,
  size,
  color,
  strokeWidth = 1.5,
}: {
  name: IconName
  size: number
  color: string
  strokeWidth?: number
}) {
  return (
    <Svg width={size} height={size} viewBox="0 0 24 24" accessible={false}>
      {ICON_PATHS[name].map((d, i) => (
        <Path
          key={i}
          d={d}
          stroke={color}
          strokeWidth={strokeWidth}
          fill="none"
          strokeLinecap="round"
          strokeLinejoin="round"
        />
      ))}
    </Svg>
  )
}
export type DisclosureItem = {
  id?: string
  label: string
  icon: (props: { size: number; color: string }) => React.ReactNode
  onPress?: () => void
}
export type CreateNewDisclosureProps = {
  items?: DisclosureItem[]
  initialOpen?: boolean
  onSelect?: (item: DisclosureItem, index: number) => void
  theme?: "light" | "dark" | "system"
  label?: string
  backgroundColor?: string
  gridColor?: string
  textColor?: string
  iconColor?: string
  hoveredIconColor?: string
  hoveredItemColor?: string
  expandedWidth?: number
  collapsedRadius?: number
  expandedRadius?: number
  itemRadius?: number
  style?: StyleProp<ViewStyle>
  testID?: string
}
const DEFAULT_ITEMS: DisclosureItem[] = (
  [
    ["project", "Project"],
    ["task", "Task"],
    ["note", "Note"],
    ["goal", "Goal"],
    ["milestone", "Milestone"],
    ["reminder", "Reminder"],
  ] as const
).map(([name, label]) => ({
  id: name,
  label,
  icon: (props) => <Icon name={name} {...props} />,
}))
function GridItem({
  item,
  tablet,
  text,
  icon,
  hoveredIcon,
  hoveredBackground,
  radius,
  onPress,
  testID,
}: {
  item: DisclosureItem
  tablet: boolean
  text: string
  icon: string
  hoveredIcon: string
  hoveredBackground: string
  radius: number
  onPress: () => void
  testID?: string
}) {
  const hover = useSharedValue(0)
  const background = useAnimatedStyle(() => ({
    backgroundColor: interpolateColor(
      hover.get(),
      [0, 1],
      ["transparent", hoveredBackground]
    ),
  }))
  const normal = useAnimatedStyle(() => ({ opacity: 1 - hover.get() }))
  const hovered = useAnimatedStyle(() => ({ opacity: hover.get() }))
  const size = tablet ? 28 : 20
  return (
    <Animated.View style={[{ flex: 1, borderRadius: radius }, background]}>
      <Pressable
        accessibilityRole="button"
        accessibilityLabel={item.label}
        onPress={onPress}
        onHoverIn={() => hover.set(withTiming(1, HOVER))}
        onHoverOut={() => hover.set(withTiming(0, HOVER))}
        onPressIn={() => {
          if (Platform.OS !== "web") hover.set(withTiming(1, HOVER))
        }}
        onPressOut={() => {
          if (Platform.OS !== "web") hover.set(withTiming(0, HOVER))
        }}
        testID={testID}
        style={{
          flex: 1,
          alignItems: "center",
          justifyContent: "center",
          paddingHorizontal: 4,
          paddingVertical: tablet ? 16 : 12,
          gap: tablet ? 6 : 4,
        }}
      >
        <View style={{ width: size, height: size }}>
          <Animated.View style={[StyleSheet.absoluteFill, normal]}>
            {item.icon({ size, color: icon })}
          </Animated.View>
          <Animated.View style={[StyleSheet.absoluteFill, hovered]}>
            {item.icon({ size, color: hoveredIcon })}
          </Animated.View>
        </View>
        <Text
          style={{
            fontFamily: FONT,
            fontSize: tablet ? 14 : 12,
            lineHeight: tablet ? 21 : 18,
            fontWeight: "500",
            letterSpacing: -(tablet ? 14 : 12) * 0.025,
            color: text,
          }}
        >
          {item.label}
        </Text>
      </Pressable>
    </Animated.View>
  )
}
export function CreateNewDisclosure({
  items = DEFAULT_ITEMS,
  initialOpen = false,
  onSelect,
  theme = "system",
  label = "Create New",
  backgroundColor,
  gridColor,
  textColor,
  iconColor,
  hoveredIconColor,
  hoveredItemColor,
  expandedWidth: widthProp,
  collapsedRadius = 32,
  expandedRadius = 22,
  itemRadius,
  style,
  testID,
}: CreateNewDisclosureProps) {
  const system = useColorScheme(),
    { width: viewport } = useWindowDimensions()
  const dark = theme === "dark" || (theme === "system" && system === "dark"),
    tablet = viewport >= 640
  const [open, setOpen] = useState(initialOpen)
  const [available, setAvailable] = useState(viewport - 32)
  const [closedText, setClosedText] = useState(0),
    [expandedText, setExpandedText] = useState(0)
  const [bodyHeight, setBodyHeight] = useState(tablet ? 270 : 220)
  const initialized = useRef(false)
  const padding = tablet ? 32 : 24,
    glyph = tablet ? 26 : 24,
    line = tablet ? 28 : 24
  const closedGroup = glyph + 8 + (closedText || (tablet ? 95 : 84))
  const expandedGroup = expandedText || (tablet ? 87 : 82)
  const closedWidth = closedGroup + padding * 2,
    closedHeight = line + (tablet ? 32 : 28)
  const expandedWidth = Math.min(
    widthProp ?? (tablet ? 384 : viewport - 32),
    available
  )
  const width = useSharedValue(initialOpen ? expandedWidth : closedWidth)
  const height = useSharedValue(initialOpen ? bodyHeight : closedHeight)
  const p = useSharedValue(initialOpen ? 1 : 0),
    labelProgress = useSharedValue(initialOpen ? 1 : 0),
    exitOpacity = useSharedValue(0)
  const bodyOpacity = useSharedValue(initialOpen ? 1 : 0)
  const scale = useSharedValue(1)
  const ready = closedText > 0 && expandedText > 0
  useEffect(() => {
    if (!initialized.current) {
      width.set(open ? expandedWidth : closedWidth)
      height.set(open ? bodyHeight : closedHeight)
      p.set(open ? 1 : 0)
      labelProgress.set(open ? 1 : 0)
      initialized.current = ready
      return
    }
    width.set(withTiming(open ? expandedWidth : closedWidth, SPRING))
    height.set(withTiming(open ? bodyHeight : closedHeight, SPRING))
    p.set(withTiming(open ? 1 : 0, SPRING))
    labelProgress.set(withTiming(open ? 1 : 0, LABEL))
  }, [
    open,
    expandedWidth,
    closedWidth,
    closedHeight,
    bodyHeight,
    ready,
    width,
    height,
    p,
    labelProgress,
  ])
  const previousOpen = useRef(initialOpen)
  useEffect(() => {
    if (previousOpen.current === open) return
    previousOpen.current = open
    exitOpacity.set(1)
    exitOpacity.set(withTiming(0, EXIT))
    bodyOpacity.set(open ? 0 : 1)
    bodyOpacity.set(withTiming(open ? 1 : 0, open ? SPRING : EXIT))
  }, [open, exitOpacity, bodyOpacity])
  const bodyText = textColor ?? (dark ? "#a3a3a3" : "#4a4a4a")
  const title = textColor ?? (dark ? "#a3a3a3" : "#5c5a56")
  const closedTitle = textColor ?? (dark ? "#a3a3a3" : "#626360")
  const root = useAnimatedStyle(() => ({
    width: width.get(),
    height: height.get(),
    borderRadius: Math.max(
      0,
      collapsedRadius + (expandedRadius - collapsedRadius) * p.get()
    ),
  }))
  const closedFace = useAnimatedStyle(() => ({
    opacity: open ? exitOpacity.get() : 1,
  }))
  const expandedFace = useAnimatedStyle(() => ({
    opacity: open ? bodyOpacity.get() : exitOpacity.get(),
  }))
  const body = useAnimatedStyle(() => ({
    opacity: bodyOpacity.get(),
    transform: [
      { scaleX: width.get() / expandedWidth },
      { scaleY: height.get() / bodyHeight },
    ],
  }))
  const sharedLabel = useAnimatedStyle(() => ({
    left: padding + (20 - padding) * labelProgress.get(),
    top:
      (tablet ? 16 : 14) +
      ((tablet ? 18 : 18.75) - (tablet ? 16 : 14)) * labelProgress.get(),
    width: closedGroup + (expandedGroup - closedGroup) * labelProgress.get(),
    height: line + ((tablet ? 24 : 22.5) - line) * labelProgress.get(),
  }))
  const closedLabel = useAnimatedStyle(() => ({
    opacity: open ? exitOpacity.get() : 1,
    transform: [
      {
        scaleX:
          (closedGroup + (expandedGroup - closedGroup) * labelProgress.get()) /
          closedGroup,
      },
      {
        scaleY:
          (line + ((tablet ? 24 : 22.5) - line) * labelProgress.get()) / line,
      },
    ],
  }))
  const expandedLabel = useAnimatedStyle(() => ({
    opacity: open ? bodyOpacity.get() : exitOpacity.get(),
    transform: [
      {
        scaleX:
          (closedGroup + (expandedGroup - closedGroup) * labelProgress.get()) /
          expandedGroup,
      },
      {
        scaleY:
          (line + ((tablet ? 24 : 22.5) - line) * labelProgress.get()) /
          (tablet ? 24 : 22.5),
      },
    ],
  }))
  const pressed = useAnimatedStyle(() => ({
    transform: [{ scale: scale.get() }],
  }))
  const shadow: ViewStyle =
    Platform.OS !== "android" || Number(Platform.Version) >= 28
      ? {
          boxShadow: [
            { offsetX: 0, offsetY: 1, blurRadius: 3, color: "rgba(0,0,0,.1)" },
            {
              offsetX: 0,
              offsetY: 1,
              blurRadius: 2,
              spreadDistance: -1,
              color: "rgba(0,0,0,.1)",
            },
          ],
        }
      : { elevation: 1 }
  const rows = Array.from({ length: Math.ceil(items.length / 3) }, (_, i) =>
    items.slice(i * 3, i * 3 + 3)
  )
  return (
    <View
      testID={testID}
      onLayout={({ nativeEvent }) => setAvailable(nativeEvent.layout.width)}
      style={[{ width: "100%", alignItems: "center" }, style]}
    >
      <Text
        aria-hidden
        accessibilityElementsHidden
        importantForAccessibility="no-hide-descendants"
        style={{
          position: "absolute",
          opacity: 0,
          fontFamily: FONT,
          fontSize: tablet ? 18 : 16,
          lineHeight: line,
          fontWeight: "500",
        }}
        onLayout={({ nativeEvent }) =>
          setClosedText(Math.ceil(nativeEvent.layout.width) + 1)
        }
      >
        {label}
      </Text>
      <Text
        aria-hidden
        accessibilityElementsHidden
        importantForAccessibility="no-hide-descendants"
        style={{
          position: "absolute",
          opacity: 0,
          fontFamily: FONT,
          fontSize: tablet ? 16 : 15,
          lineHeight: tablet ? 24 : 22.5,
          fontWeight: "600",
        }}
        onLayout={({ nativeEvent }) =>
          setExpandedText(Math.ceil(nativeEvent.layout.width) + 1)
        }
      >
        {label}
      </Text>
      <Animated.View style={[{ overflow: "hidden" }, root]}>
        <Animated.View
          pointerEvents="none"
          style={[
            StyleSheet.absoluteFill,
            {
              backgroundColor:
                backgroundColor ?? (dark ? "#171717" : "#fafbf8"),
              zIndex: open ? 0 : 1,
            },
            closedFace,
          ]}
        />
        <Animated.View
          pointerEvents="none"
          style={[
            StyleSheet.absoluteFill,
            {
              backgroundColor:
                backgroundColor ?? (dark ? "#171717" : "#f7f5ee"),
              zIndex: open ? 1 : 0,
            },
            expandedFace,
          ]}
        />
        <Animated.View
          pointerEvents={open ? "auto" : "none"}
          aria-hidden={!open}
          accessibilityElementsHidden={!open}
          importantForAccessibility={open ? "auto" : "no-hide-descendants"}
          onLayout={({ nativeEvent }) =>
            setBodyHeight(nativeEvent.layout.height)
          }
          style={[
            {
              position: "absolute",
              top: 0,
              left: 0,
              width: expandedWidth,
              padding: 4,
              transformOrigin: "top left",
              zIndex: 2,
            },
            body,
          ]}
        >
          <View
            style={{
              height: 52,
              paddingHorizontal: 16,
              paddingVertical: 14,
              alignItems: "flex-end",
              justifyContent: "center",
            }}
          >
            <Animated.View style={pressed}>
              <Pressable
                accessibilityRole="button"
                accessibilityLabel="Close create menu"
                onPress={() => setOpen(false)}
                onPressIn={() => scale.set(withSpring(0.95, PRESS))}
                onPressOut={() => scale.set(withSpring(1, PRESS))}
                testID={testID ? `${testID}-close` : undefined}
                style={{
                  width: 24,
                  height: 24,
                  borderRadius: 999,
                  backgroundColor: dark ? "#404040" : "#b8b5b0",
                  alignItems: "center",
                  justifyContent: "center",
                }}
              >
                <Icon name="close" size={16} color="#fff" strokeWidth={2.5} />
              </Pressable>
            </Animated.View>
          </View>
          <View
            style={[
              {
                padding: tablet ? 16 : 12,
                gap: 4,
                borderRadius: 20,
                backgroundColor: gridColor ?? (dark ? "#0a0a0a" : "#fff"),
              },
              shadow,
            ]}
          >
            {rows.map((row, rowIndex) => (
              <View key={rowIndex} style={{ flexDirection: "row", gap: 4 }}>
                {row.map((item, column) => (
                  <GridItem
                    key={item.id ?? column}
                    item={item}
                    tablet={tablet}
                    text={bodyText}
                    icon={iconColor ?? (dark ? "#737373" : "#8b8b8b")}
                    hoveredIcon={
                      hoveredIconColor ?? (dark ? "#d4d4d4" : "#4a4a4a")
                    }
                    hoveredBackground={
                      hoveredItemColor ??
                      (dark ? "rgba(38,38,38,.5)" : "#f4f2ea")
                    }
                    radius={itemRadius ?? (tablet ? 24 : 20)}
                    onPress={() => {
                      item.onPress?.()
                      onSelect?.(item, rowIndex * 3 + column)
                    }}
                    testID={
                      testID
                        ? `${testID}-item-${rowIndex * 3 + column}`
                        : undefined
                    }
                  />
                ))}
                {Array.from({ length: 3 - row.length }, (_, i) => (
                  <View key={`empty-${i}`} style={{ flex: 1 }} />
                ))}
              </View>
            ))}
          </View>
        </Animated.View>
        <Animated.View
          pointerEvents="none"
          aria-hidden
          accessibilityElementsHidden
          importantForAccessibility="no-hide-descendants"
          style={[{ position: "absolute", zIndex: 3 }, sharedLabel]}
        >
          <Animated.View
            style={[
              {
                position: "absolute",
                width: closedGroup,
                height: line,
                flexDirection: "row",
                alignItems: "center",
                gap: 8,
                transformOrigin: "top left",
              },
              closedLabel,
            ]}
          >
            <Icon name="plus" size={glyph} color={closedTitle} />
            <Text
              numberOfLines={1}
              style={{
                fontFamily: FONT,
                fontSize: tablet ? 18 : 16,
                lineHeight: line,
                fontWeight: "500",
                color: closedTitle,
              }}
            >
              {label}
            </Text>
          </Animated.View>
          <Animated.Text
            numberOfLines={1}
            style={[
              {
                position: "absolute",
                width: expandedGroup,
                height: tablet ? 24 : 22.5,
                fontFamily: FONT,
                fontSize: tablet ? 16 : 15,
                lineHeight: tablet ? 24 : 22.5,
                fontWeight: "600",
                color: title,
                transformOrigin: "top left",
              },
              expandedLabel,
            ]}
          >
            {label}
          </Animated.Text>
        </Animated.View>
        {!open && (
          <Pressable
            accessibilityRole="button"
            accessibilityLabel={label}
            accessibilityState={{ expanded: false }}
            onPress={() => setOpen(true)}
            testID={testID ? `${testID}-open` : undefined}
            style={[StyleSheet.absoluteFill, { zIndex: 4 }]}
          />
        )}
      </Animated.View>
    </View>
  )
}

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

Back to Watermelon UI