Skip to content
  • Introduction
    nativecn is shadcn for Expo apps: Components you copy into your app and own, built so AI agents can build and verify whole mobile apps.
  • Installation
    Create a new Expo app with nativecn, or add nativecn to an existing one.
  • components.json
    The project file that records your Preset, Structure, aliases and agents.
  • CLI
    Every nativecn-cli command and flag: init, create, add, agents and mcp.
  • Changelog
    What changed in each nativecn release.
  • Theming
    Tokens, Colour Roles, Scale, Scheme, createStyles and fonts.
  • Presets & Styles
    Style, Base Colour, Accent Colour, Radius and fonts: chosen once, baked into your code.
  • Folder structure
    Flat and feature Structures, Destinations and the placement rule.
  • AI agents
    Rules, Skills, the nativecn MCP server, the Plugin and the Visual QA Loop.
  • Alert
    Alert, AlertTitle and AlertDescription: an inline message with Variants (default, destructive, success, warning, info), each with a default icon.
  • Avatar
    Round user image (expo-image) with fallback initials, in sizes sm, md and lg.
  • Badge
    Small status label with Variants default, secondary, outline, destructive, success and warning.
  • Button
    Button with Variants (primary, secondary, outline, ghost, destructive, link), Sizes, an icon slot, loading and status; icon-only requires aria-label.
  • Card
    Card surface with CardHeader, CardTitle, CardDescription, CardContent and CardFooter; onPress makes the whole card pressable.
  • Checkbox
    Checkbox with an optional label, controlled or uncontrolled, an indeterminate (mixed) state and a haptic tick.
  • Chip
    Chip (plain, toggle or removable, with an optional icon) and ChipGroup (single or multiple choice on the SelectionGroup Primitive), with a haptic tick.
  • Container
    Wraps a Screen: safe-area edges, scrolling, keyboard avoidance, Token padding and a max content width.
  • Drawer
    Drawer panel content for Expo Router's drawer (expo-router/drawer): DrawerContent, DrawerHeader, DrawerSection, DrawerItem (label, icon, href, badge; active from the current route) and DrawerFooter.
  • Empty State
    What a Screen or list shows when there is nothing to show yet: an icon, a title, a description and an action.
  • FormField
    Lays out a control with its Label, description and error, and links them for screen readers ("Email, text field, Enter a valid email").
  • Icon
    Every glyph: a Lucide icon at a Token size in a Colour Role, decorative unless labelled.
  • Input
    Single-line text field with Sizes, a leading icon, error/success status, a focus ring and an automatic Show/Hide password toggle.
  • InputOTP
    One-time code field: one hidden input drives N cells; numeric keyboard, SMS/Keychain autofill, paste, onComplete, error/success status and secure.
  • Label
    A field's visible label in the label type Variant, with a required marker read as "required".
  • List
    Settings-style rows: ListSection, ListSectionHeader, ListSectionFooter and ListItem (title, description, icon, trailing, chevron, destructive, onPress); FlashList-safe.
  • Progress
    A horizontal progress bar: a value 0–100 that animates, or indeterminate; role progressbar with its value.
  • RadioGroup
    RadioGroup and RadioGroupItem: a single choice from a list on the SelectionGroup Primitive, vertical or horizontal, with a haptic tick.
  • SchemeSwitcher
    Theme switcher / dark mode toggle: System, Light and Dark as SegmentedTabs, or one icon button cycling through them. Calls setScheme, which persists the choice.
  • SearchField
    Search box with a search icon, an automatic clear (×) button, a loading spinner and onSubmit from the keyboard's Search key.
  • SegmentedTabs
    In-screen tabs (not navigation): segmented or underline Variants, an animated indicator, and only the selected tab's content rendered.
  • Separator
    A hairline divider, horizontal or vertical, decorative by default.
  • Skeleton
    A placeholder shape shown while content loads; pulses unless Reduce Motion is on, hidden from screen readers.
  • Slider
    Pick a number in a range by dragging or tapping (Gesture Handler + Reanimated), snapped to steps with a haptic tick; adjustable for screen readers.
  • Spinner
    An indeterminate loading indicator in a Colour Role, announced as busy; still under Reduce Motion.
  • Switch
    A themed on/off Switch, identical on iOS and Android, with an optional label, a thumb that slides (instant under Reduce Motion) and a haptic tick.
  • TabNavigation
    The bottom tab bar: the custom tabBar of an Expo Router JS Tabs Layout, with classic and floating Variants, per-tab icons and badges from Tabs.Screen options.
  • Text
    All text: the type ramp as Variants, text Colour Roles, alignment and the font-scaling switch.
  • Textarea
    Multi-line text field that grows from minRows to maxRows, with a character counter when maxLength is set.
  • Toast
    Sonner-style Toasts: a <Toaster /> in the root Layout and toast(), toast.success/error/info/warning/dismiss. At most 3 stacked, 4s auto-dismiss, swipe away, announced, above native modals.
  • Profile header + grouped sections
    Drawer panel with a profile header (avatar, name, email, plan Badge), labelled sections (Main, Workspace, Support) and Log out pinned in the footer.
  • SaaS workspace
    Drawer panel for a SaaS workspace: logo and name with a close button, an Accent Colour onboarding item with a 5 Steps Badge, main items, a bottom group (Notifications with a count, Settings, Docs, Help) and a footer with a team switcher and the user's avatar.
  • Cover header
    Drawer panel with a dark cover (large avatar, name, View profile, a bell with a count, an overflow button and a round + action), icon items active in Accent Colour text, a divider and text-only secondary items.
  • Classic centred
    A centred sign-in Screen: logo, email and password, Forgot password, an "or" divider, then Apple and Google, and a Sign up link.
  • Social-first hero
    A social-first sign-in Screen: a dark brand hero (logo, headline, subtitle, centred) above a panel where Apple and Google lead and "Continue with email" reveals the email form.
  • Email-first, two steps
    An email-first sign-in Screen in two steps: the email, then the password, with "Email me a code instead" switching to a 6-digit code (InputOTP). Lucide chevron-left Back buttons.
  • Classic single form
    A classic sign-up Screen on one scroll: Apple and Google on top, "or", then name, email and password, a terms Checkbox with Terms and Privacy Policy links, Create account, and a Sign in link.
  • Step-by-step wizard
    A sign-up wizard, one question per step under a Progress bar: email, name, password, then a 6-digit code (InputOTP). Lucide chevron-left Back buttons; the Continue button rides above the keyboard.
  • Social-first + verify
    A brand-led, passwordless sign-up Screen: the logo and headline centred above Apple and Google; "Sign up with email" reveals the email field, and Send code moves on to a 6-digit code (InputOTP).
  • announce
    Reliable screen-reader announcements on iOS and Android (Button and Input status, Toast, error, success and warning Alerts).
  • FocusChain
    Automatic Next/Done between fields in on-screen order; Done submits the form.
  • FormField context
    Links a field to its label, description and error for screen readers; passes status, disabled and required down.
  • Keyboard
    KeyboardProvider setup, a scroll view that keeps the focused field above the keyboard, and a footer that keeps a form's main button above it.
  • Portal
    Renders content above every Screen (above native modals on iOS), layered and safe-area aware.
  • Pressable
    Pressed look from a Style Slot, tap area extended to 48, no handlers while disabled or loading, optional haptics, role and aria-* states.
  • SelectionGroup
    Single or multiple choice within a group, with radio, tab or checkbox roles and selected states.
  • useControllableState
    One implementation of controlled and uncontrolled values for form controls.
  • useMotion
    Enter/exit animations and timing/spring configs from the motion Tokens; instant when Reduce Motion is on.
  • Theme
    Tokens, Scale, Colour Roles, ThemeProvider, createStyles and the navigation theme.

Drawer

Drawer panel content for Expo Router's drawer (expo-router/drawer): DrawerContent, DrawerHeader, DrawerSection, DrawerItem (label, icon, href, badge; active from the current route) and DrawerFooter.

View as Markdown
Componentnavigation
Style

Light

Dark

Your app is ready.

Inter body text, Inter headings.

smmdlgxl
you@example.com
Continue
This needs Expo Router (a navigation Layout), so it can't run in the browser. The card shows your Preset's Tokens; run it in an app to see it live.Default Preset a0: Vega, neutral, neutral accent, default radius, Inter.Change Preset

Installation

$npx nativecn-cli@latest add drawer

Also installs: theme, pressable, text, icon, badge.

Packages: expo-router, react-native-safe-area-context.

Usage

Drawer demo

An app/(drawer)/_layout.tsx: a profile header, titled sections, an accent item, a bottom group and a Log out footer.

// app/(drawer)/_layout.tsx: a drawer Layout whose panel is built from the drawer Components.
// The drawer comes only from `expo-router/drawer` (built into Expo Router); never install
// `@react-navigation/drawer` or the unrelated npm package `expo-drawer`.
import { Drawer } from "expo-router/drawer";
import { Bell, Home, Inbox, LifeBuoy, LogOut, Settings, Users } from "lucide-react-native";
import { View } from "react-native";

import { Avatar } from "@/components/avatar";
import { Badge } from "@/components/badge";
import {
  DrawerContent,
  DrawerFooter,
  DrawerHeader,
  DrawerItem,
  DrawerSection,
} from "@/components/drawer";
import { Separator } from "@/components/separator";
import { Text } from "@/components/text";
import { createStyles } from "@/theme";

export default function DrawerLayout() {
  return (
    <Drawer
      drawerContent={(props) => (
        <DrawerContent {...props}>
          <DrawerHeader>
            <ProfileHeader />
          </DrawerHeader>

          <DrawerSection title="Main">
            <DrawerItem label="Home" icon={Home} href="/" />
            <DrawerItem label="Inbox" icon={Inbox} href="/inbox" badge={3} />
            <DrawerItem label="Team" icon={Users} href="/team" />
          </DrawerSection>

          {/* An Accent Colour highlighted item, and an item active in Accent Colour text. */}
          <DrawerSection title="Workspace">
            <DrawerItem
              label="Get started"
              href="/onboarding"
              tone="accent"
              badge={<Badge label="5 Steps" />}
            />
            <DrawerItem label="Notifications" icon={Bell} href="/notifications" variant="text" />
          </DrawerSection>

          {/* Pushed to the bottom of the scrolling area, above the footer. */}
          <DrawerSection style={{ marginTop: "auto" }}>
            <DrawerItem label="Settings" icon={Settings} href="/settings" />
            <DrawerItem label="Help" icon={LifeBuoy} href="/help" />
          </DrawerSection>

          <DrawerFooter>
            <Separator />
            <DrawerItem label="Log out" icon={LogOut} onPress={() => {}} />
          </DrawerFooter>
        </DrawerContent>
      )}
    >
      <Drawer.Screen name="index" options={{ title: "Home" }} />
    </Drawer>
  );
}

function ProfileHeader() {
  const styles = useStyles();
  return (
    <View style={styles.profile}>
      <Avatar fallback="JD" size="lg" />
      <View style={styles.name}>
        <Text variant="h4">Jane Doe</Text>
        <Text variant="small" color="mutedForeground">
          jane@example.com
        </Text>
      </View>
      <Badge label="Pro" variant="secondary" />
    </View>
  );
}

const useStyles = createStyles((t) => ({
  profile: { gap: t.spacing[3] },
  name: { gap: t.spacing[1] },
}));

Props

DrawerContent

PropType and description
...DrawerContentComponentPropsExpo Router's drawer props (state, navigation, descriptors): spread the drawerContent props in
childrenDrawerHeader (pinned on top), DrawerSections and DrawerItems (scrolling) and DrawerFooter (pinned at the bottom), in any order
contentContainerStylethe scrolling area's content, merged last
stylemerged last onto the panel

DrawerHeader

PropType and description
childrena profile, a logo and name, or a cover
stylemerged last; the top padding includes the status bar inset

DrawerSection

PropType and description
titlestring: a small muted heading (role heading)
childrenDrawerItems
stylemerged last; { marginTop: "auto" } pushes the group to the bottom

DrawerItem

PropType and description
labelstring (required): the visible text and accessible name
icona Lucide icon component; leave it out for text-only items
hrefHref: pressing navigates there and closes the drawer; active while the pathname is this route or below it
badgestring | number (a secondary Badge) | ReactElement (rendered as-is)
activeboolean: overrides the active state derived from the route
variant"default" (default): active on a muted background | "text": active in the Accent Colour, no background
tone"default" (default) | "accent": an Accent Colour tint with Accent Colour text and icon
onPress(event) => void: runs before navigating; without href the item is an action (role button) and the drawer stays open
disabledboolean (default false)
stylelayout only, merged last onto the root

DrawerFooter

PropType and description
childrenLog out, a team switcher, the user
stylemerged last; the bottom padding includes the home indicator inset

Variants

  • variant: default, text
  • tone: default, accent

Accessibility

  • A DrawerItem with href has role link, with aria-selected while its route is active; without href it has role button.
  • An item's visible label is its accessible name.
  • Items are 48 high in Vega and 40 in Nova, with the tap area extended to 48.
  • DrawerSection titles have role heading.
  • disabled items are announced as disabled and block presses.
  • Item labels use the label text Variant, which caps the OS font scale at 1.5× when font scaling is on.

Notes

Built only on expo-router/drawer, which ships inside Expo Router (react-native-drawer-layout): never install @react-navigation/drawer or the npm package expo-drawer. In app/(drawer)/_layout.tsx render <Drawer drawerContent={(props) => <DrawerContent {...props}>…</DrawerContent>} />. A DrawerItem with href is active while usePathname() is its route or below it (route groups such as (drawer) and index are ignored); tapping it calls router.navigate(href) and closes the drawer. Items are sized by the drawer.item Style Slot (Vega 48, Nova 40); the label, pressed look and accent tint come from drawer.label, drawer.pressed and drawer.tint. The panel keeps clear of the status bar and home indicator.

Source

The Vega Style's code, exactly as add copies it into your app.

{components}/drawer.tsx

import { router, usePathname, type Href } from "expo-router";
import type { DrawerContentComponentProps } from "expo-router/drawer";
import type { LucideIcon } from "lucide-react-native";
import {
  Children,
  createContext,
  isValidElement,
  use,
  type ReactElement,
  type ReactNode,
  type Ref,
} from "react";
import {
  ScrollView,
  View,
  type GestureResponderEvent,
  type StyleProp,
  type ViewProps,
  type ViewStyle,
} from "react-native";
import { SafeAreaInsetsContext, initialWindowMetrics } from "react-native-safe-area-context";

import { Badge } from "@/components/badge";
import { Icon } from "@/components/icon";
import {
  Pressable,
  touchTargetHitSlop,
  type PressableHaptic,
} from "@/components/primitives/pressable";
import { Text, type TextProps } from "@/components/text";
import { createStyles, useTheme } from "@/theme";

/** How an item shows that it is the current route. */
export type DrawerItemVariant = "default" | "text";
/** `accent` tints the item with the Accent Colour whether or not it is active. */
export type DrawerItemTone = "default" | "accent";

/** The drawer's navigation, handed down by DrawerContent so items can close the drawer. */
type DrawerContextValue = { navigation?: DrawerContentComponentProps["navigation"] };
const DrawerContext = createContext<DrawerContextValue>({});

export type DrawerContentProps = Partial<DrawerContentComponentProps> &
  ViewProps & {
    /**
     * DrawerHeader (pinned to the top), DrawerSections and DrawerItems (scrolling), and
     * DrawerFooter (pinned to the bottom), in any order.
     */
    children?: ReactNode;
    /** Style for the scrolling area's content, merged after its own padding and gap. */
    contentContainerStyle?: StyleProp<ViewStyle>;
    ref?: Ref<View>;
  };

export type DrawerHeaderProps = ViewProps & { children?: ReactNode; ref?: Ref<View> };
export type DrawerFooterProps = ViewProps & { children?: ReactNode; ref?: Ref<View> };

export type DrawerSectionProps = ViewProps & {
  /** A small muted heading above the section's items (role `heading`). */
  title?: string;
  children?: ReactNode;
  ref?: Ref<View>;
};

export type DrawerSectionTitleProps = Omit<TextProps, "variant">;

export type DrawerItemProps = Omit<ViewProps, "style" | "hitSlop" | "children"> & {
  /** The visible text and the accessible name. */
  label: string;
  /** A Lucide icon at the start of the item. Leave it out for text-only items. */
  icon?: LucideIcon;
  /**
   * The route to open. Pressing navigates there and closes the drawer; the item is active
   * while the current pathname is this route or below it. Groups like `(drawer)` are ignored.
   */
  href?: Href;
  /** At the end of the item: a string or number renders a secondary Badge; any node as-is. */
  badge?: string | number | ReactElement;
  /** Overrides the active state derived from the current route. */
  active?: boolean;
  /** `default`: active on a muted background. `text`: active in the Accent Colour, no background. */
  variant?: DrawerItemVariant;
  /** `accent`: a highlighted item on an Accent Colour tint, with Accent Colour text and icon. */
  tone?: DrawerItemTone;
  /** Runs before navigating. Items without `href` are actions (role `button`) and stay open. */
  onPress?: (event: GestureResponderEvent) => void;
  onLongPress?: (event: GestureResponderEvent) => void;
  /** Blocks presses, dims the item and announces it as disabled. */
  disabled?: boolean;
  /** Haptic feedback on press (when `config.haptics` is on). */
  haptic?: PressableHaptic;
  /** Layout only, merged last onto the root. */
  style?: StyleProp<ViewStyle>;
  ref?: Ref<View>;
};

const useStyles = createStyles((t) => {
  const item = {
    height: t.scaleValue(48),
    borderRadius: t.radius.md,
    paddingHorizontal: t.spacing[3],
    gap: t.spacing[3],
  };
  const label = { ...t.type.label };
  const tint = { backgroundColor: t.colors.primary, opacity: 0.12 };
  return {
    root: { flex: 1, backgroundColor: t.colors.card },
    scroll: { flex: 1 },
    content: {
      flexGrow: 1,
      paddingHorizontal: t.spacing[3],
      paddingVertical: t.spacing[3],
      gap: t.spacing[4],
    },
    header: { paddingHorizontal: t.spacing[4], paddingBottom: t.spacing[4], gap: t.spacing[3] },
    footer: { paddingHorizontal: t.spacing[3], paddingTop: t.spacing[3], gap: t.spacing[1] },
    section: { gap: t.spacing[1] },
    sectionTitle: {
      paddingHorizontal: item.paddingHorizontal,
      paddingVertical: t.spacing[1],
    },
    item: {
      ...item,
      flexDirection: "row",
      alignItems: "center",
      borderCurve: "continuous",
      overflow: "hidden",
    },
    label: { ...label, flex: 1, minWidth: 0 },
    pressed: { backgroundColor: t.colors.muted },
    active: { backgroundColor: t.colors.muted },
    tint: {
      ...tint,
      position: "absolute",
      top: 0,
      bottom: 0,
      start: 0,
      end: 0,
      pointerEvents: "none",
    },
    disabled: { opacity: t.opacity.disabled },
    badge: { flexShrink: 0 },
  };
});

const isElementOf = (node: ReactNode, type: unknown): node is ReactElement =>
  isValidElement(node) && node.type === type;

function useInsets() {
  return use(SafeAreaInsetsContext) ?? initialWindowMetrics?.insets;
}

/**
 * The drawer panel's content: pass it Expo Router's drawer props, as in
 * `<Drawer drawerContent={(props) => <DrawerContent {...props}>…</DrawerContent>} />` from
 * `expo-router/drawer`. A DrawerHeader child is pinned to the top and a DrawerFooter to the
 * bottom, wherever they appear; everything else scrolls between them. The panel keeps clear of
 * the status bar and home indicator. `style` is merged last onto the root.
 */
export function DrawerContent({
  state: _state,
  descriptors: _descriptors,
  navigation,
  children,
  contentContainerStyle,
  style,
  ...props
}: DrawerContentProps) {
  const styles = useStyles();
  const insets = useInsets();
  const all = Children.toArray(children);
  const headers = all.filter((c) => isElementOf(c, DrawerHeader));
  const footers = all.filter((c) => isElementOf(c, DrawerFooter));
  const body = all.filter((c) => !isElementOf(c, DrawerHeader) && !isElementOf(c, DrawerFooter));

  // DrawerHeader and DrawerFooter pad the safe area themselves; without them, the scroll does.
  const safeArea: ViewStyle = {
    paddingTop: headers.length ? undefined : (insets?.top ?? 0) + styles.content.paddingVertical,
    paddingBottom: footers.length
      ? undefined
      : (insets?.bottom ?? 0) + styles.content.paddingVertical,
  };

  return (
    <DrawerContext value={{ navigation }}>
      <View style={[styles.root, style]} {...props}>
        {headers}
        <ScrollView
          style={styles.scroll}
          contentContainerStyle={[styles.content, safeArea, contentContainerStyle]}
        >
          {body}
        </ScrollView>
        {footers}
      </View>
    </DrawerContext>
  );
}

/**
 * The top of the panel (a profile, a logo and name, a cover…), pinned above the scrolling
 * items. Its top padding includes the status bar inset, so a background set with `style`
 * reaches the top edge. `style` is merged last.
 */
export function DrawerHeader({ style, ...props }: DrawerHeaderProps) {
  const styles = useStyles();
  const insets = useInsets();
  return (
    <View
      style={[
        styles.header,
        { paddingTop: (insets?.top ?? 0) + styles.header.paddingBottom },
        style,
      ]}
      {...props}
    />
  );
}

/**
 * The bottom of the panel (Log out, a team switcher, the user…), pinned below the scrolling
 * items. Its bottom padding includes the home indicator inset. `style` is merged last.
 */
export function DrawerFooter({ style, ...props }: DrawerFooterProps) {
  const styles = useStyles();
  const insets = useInsets();
  return (
    <View
      style={[
        styles.footer,
        { paddingBottom: (insets?.bottom ?? 0) + styles.footer.paddingTop },
        style,
      ]}
      {...props}
    />
  );
}

/**
 * A group of DrawerItems with an optional `title`. To push a group to the bottom of the
 * scrolling area (above the footer), pass `style={{ marginTop: "auto" }}`. `style` is merged
 * last.
 */
export function DrawerSection({ title, children, style, ...props }: DrawerSectionProps) {
  const styles = useStyles();
  return (
    <View style={[styles.section, style]} {...props}>
      {title ? <DrawerSectionTitle>{title}</DrawerSectionTitle> : null}
      {children}
    </View>
  );
}

/** A DrawerSection's heading; use it directly only for a custom section layout. */
export function DrawerSectionTitle({ style, ...props }: DrawerSectionTitleProps) {
  const styles = useStyles();
  return (
    <Text
      role="heading"
      variant="caption"
      color="mutedForeground"
      style={[styles.sectionTitle, style]}
      {...props}
    />
  );
}

/** Drop route groups like `(drawer)`, `index` and a trailing slash, and fill `[param]`s. */
function hrefPath(href: Href): string {
  const { pathname, params } =
    typeof href === "string" ? { pathname: href, params: undefined } : href;
  const value = (key: string) => params?.[key];
  const segments = pathname
    .split(/[?#]/)[0]!
    .split("/")
    .flatMap((segment) => {
      if (!segment || /^\(.+\)$/.test(segment) || segment === "index") return [];
      const rest = /^\[\.\.\.(.+)\]$/.exec(segment);
      if (rest) return [value(rest[1]!)].flat().map(String);
      const param = /^\[(.+)\]$/.exec(segment);
      return [param ? String(value(param[1]!) ?? segment) : segment];
    });
  return `/${segments.join("/")}`;
}

/** Active while the pathname is the item's route or one below it (`/` matches only itself). */
export function isDrawerItemActive(href: Href, pathname: string): boolean {
  const path = hrefPath(href);
  const current = pathname.length > 1 ? pathname.replace(/\/$/, "") : pathname;
  return current === path || (path !== "/" && current.startsWith(`${path}/`));
}

/**
 * One drawer entry: icon, label and an optional badge. With `href` it is a link to that route,
 * active (and `aria-selected`) while the current route matches; pressing it navigates and
 * closes the drawer. Without `href` it is an action button (Log out…) that runs `onPress`.
 * Height, radius and padding come from the `drawer.item` Style Slot; the tap area is extended to
 * 48. `style` is merged last onto the root.
 */
export function DrawerItem({
  label,
  icon,
  href,
  badge,
  active: activeProp,
  variant = "default",
  tone = "default",
  onPress,
  onLongPress,
  disabled = false,
  haptic,
  style,
  ...props
}: DrawerItemProps) {
  const styles = useStyles();
  const { minTouchTarget } = useTheme();
  const { navigation } = use(DrawerContext);
  const pathname = usePathname();
  const active = activeProp ?? (href != null && isDrawerItemActive(href, pathname));
  const accent = tone === "accent" || (active && variant === "text");
  const color = accent ? "primary" : "foreground";

  const handlePress = (event: GestureResponderEvent) => {
    onPress?.(event);
    if (href == null) return;
    if (!active) router.navigate(href);
    navigation?.closeDrawer();
  };

  const hitSlop = touchTargetHitSlop(undefined, styles.item.height as number, minTouchTarget);

  return (
    <Pressable
      role={href != null ? "link" : "button"}
      aria-selected={active}
      onPress={handlePress}
      onLongPress={onLongPress}
      disabled={disabled}
      haptic={haptic}
      hitSlop={hitSlop ?? 0}
      pressedStyle={styles.pressed}
      style={[
        styles.item,
        active && variant === "default" ? styles.active : null,
        disabled ? styles.disabled : null,
        style,
      ]}
      {...props}
    >
      {tone === "accent" ? <View style={styles.tint} /> : null}
      {icon ? <Icon icon={icon} color={color} /> : null}
      <Text variant="label" color={color} numberOfLines={1} style={styles.label}>
        {label}
      </Text>
      {badge == null ? null : typeof badge === "string" || typeof badge === "number" ? (
        <Badge label={String(badge)} variant="secondary" style={styles.badge} />
      ) : (
        badge
      )}
    </Pressable>
  );
}