diff --git a/.gitignore b/.gitignore
index 7c11bae..94dd65c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,5 +54,7 @@ msbuild.binlog
.claude/*
-# Windows reserved device names
-nul
\ No newline at end of file
+# Windows
+nul
+ReactNativeWindows.dsc.yaml
+WindowsDevTools.dsc.yaml
diff --git a/README.md b/README.md
index c08aff0..3a4b572 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,15 @@ npm run macos-release
#### System Requirements
-First, install the system requirements for React Native Windows: https://microsoft.github.io/react-native-windows/docs/rnw-dependencies
+First, install the system requirements for React Native Windows by running the following in an elevated Powershell session:
+
+```powershell
+.\bin\windows-setup.ps1
+```
+
+This should install everything for you.
+
+**Alternative**: Follow the official installation steps (results vary): https://microsoft.github.io/react-native-windows/docs/rnw-dependencies
**Alternative**: If you experience issues with the official `rnw-dependencies.ps1` script, consider using Josh Yoes' improved setup process: https://github.com/joshuayoes/ReactNativeWindowsSandbox
@@ -60,6 +68,26 @@ npm run windows
npm run windows-release
```
+**Troubleshooting**: If you run into a build error with ctype.h or ROSLYNCODETASKFACTORYCSHARPCOMPILER, try making a symlink like this (it's weird, I know):
+
+```powershell
+mklink /D "C:\Program Files\Windows Kits\10\lib" "C:\Program Files (x86)\Windows Kits\10\lib"
+```
+
+Somehow, `C:\Program Files\` instead of `C:\Program Files (x86)\` is being used, which breaks things. This makes a symlink to fix that.
+
+You may also need to add this path to the `reactotron.vcxproj`:
+
+```
+
+
+
+ $(ProjectDir)..\..\app;%(AdditionalIncludeDirectories);C:\Program Files (x86)\Windows Kits\10\Include\10.0.22621.0\ucrt
+
+
+
+```
+
### Cross-Platform Native Development
Both platforms use unified commands for native module development:
diff --git a/app/app.tsx b/app/app.tsx
index c8cae8d..d6d6846 100644
--- a/app/app.tsx
+++ b/app/app.tsx
@@ -1,25 +1,17 @@
-/**
- * Sample React Native App
- * https://github.com/facebook/react-native
- *
- * @format
- */
-import { DevSettings, NativeModules, StatusBar, View, type ViewStyle } from "react-native"
+import { StatusBar, View, type ViewStyle } from "react-native"
import { connectToServer } from "./state/connectToServer"
import { useTheme, themed } from "./theme/theme"
-import { useEffect, useMemo, useState } from "react"
+import { useEffect } from "react"
import { TimelineScreen } from "./screens/TimelineScreen"
-import { useMenuItem } from "./utils/useMenuItem"
import { Titlebar } from "./components/Titlebar/Titlebar"
import { Sidebar } from "./components/Sidebar/Sidebar"
-import { useSidebar } from "./state/useSidebar"
-import { useGlobal, withGlobal } from "./state/useGlobal"
+import { useGlobal } from "./state/useGlobal"
import { MenuItemId } from "./components/Sidebar/SidebarMenu"
import { HelpScreen } from "./screens/HelpScreen"
-import { TimelineItem } from "./types"
import { PortalHost } from "./components/Portal"
import { StateScreen } from "./screens/StateScreen"
-import { AboutModal } from "./components/AboutModal"
+import { ShortcutsProvider } from "./contexts/ShortcutsContext"
+import { SystemMenu } from "./components/SystemMenu"
import { CustomCommandsScreen } from "./screens/CustomCommandsScreen"
if (__DEV__) {
@@ -30,96 +22,7 @@ if (__DEV__) {
function App(): React.JSX.Element {
const { colors } = useTheme()
- const { toggleSidebar } = useSidebar()
- const [activeItem, setActiveItem] = useGlobal("sidebar-active-item", "logs")
- const [, setTimelineItems] = withGlobal("timelineItems", [])
- const [aboutVisible, setAboutVisible] = useState(false)
-
- const menuConfig = useMemo(
- () => ({
- remove: ["File", "Edit", "Format", "Reactotron > About Reactotron"],
- items: {
- Reactotron: [
- {
- label: "About Reactotron",
- position: 0,
- action: () => setAboutVisible(true),
- },
- ],
- View: [
- {
- label: "Toggle Sidebar",
- shortcut: "cmd+b",
- action: toggleSidebar,
- },
- {
- label: "Logs Tab",
- shortcut: "cmd+1",
- action: () => setActiveItem("logs"),
- },
- {
- label: "Network Tab",
- shortcut: "cmd+2",
- action: () => setActiveItem("network"),
- },
- {
- label: "Performance Tab",
- shortcut: "cmd+3",
- action: () => setActiveItem("performance"),
- },
- {
- label: "Plugins Tab",
- shortcut: "cmd+4",
- action: () => setActiveItem("plugins"),
- },
- {
- label: "Custom Commands Tab",
- shortcut: "cmd+5",
- action: () => setActiveItem("customCommands"),
- },
- {
- label: "Help Tab",
- shortcut: "cmd+6",
- action: () => setActiveItem("help"),
- },
- ...(__DEV__
- ? [
- {
- label: "Toggle Dev Menu",
- shortcut: "cmd+shift+d",
- action: () => NativeModules.DevMenu.show(),
- },
- ]
- : []),
- ],
- Window: [
- {
- label: "Reload",
- shortcut: "cmd+shift+r",
- action: () => DevSettings.reload(),
- },
- ],
- Tools: [
- {
- label: "Clear Timeline Items",
- shortcut: "cmd+k",
- action: () => setTimelineItems([]),
- },
- ],
- },
- }),
- [toggleSidebar, setActiveItem],
- )
-
- useMenuItem(menuConfig)
-
- setTimeout(() => {
- fetch("https://www.google.com")
- .then((res) => res.text())
- .then((text) => {
- console.tron.log("text", text)
- })
- }, 1000)
+ const [activeItem] = useGlobal("sidebar-active-item", "logs")
// Connect to the server when the app mounts.
// This will update global state with the server's state
@@ -140,16 +43,19 @@ function App(): React.JSX.Element {
}
return (
-
-
-
-
-
- {renderActiveItem()}
-
-
- setAboutVisible(false)} />
-
+
+
+
+
+
+
+
+ {renderActiveItem()}
+
+
+
+
+
)
}
diff --git a/app/components/Menu/MenuDropdown.tsx b/app/components/Menu/MenuDropdown.tsx
new file mode 100644
index 0000000..0f1a9d0
--- /dev/null
+++ b/app/components/Menu/MenuDropdown.tsx
@@ -0,0 +1,102 @@
+import { View, type ViewStyle } from "react-native"
+import { useRef, useMemo, memo } from "react"
+import { themed } from "../../theme/theme"
+import { Portal } from "../Portal"
+import { MenuDropdownItem } from "./MenuDropdownItem"
+import { useSubmenuState } from "./useSubmenuState"
+import { menuSettings } from "./menuSettings"
+import { type Position, type DropdownMenuItem, type MenuItem, MENU_SEPARATOR } from "./types"
+import { getUUID } from "../../utils/random/getUUID"
+import { Separator } from "../Separator"
+
+interface MenuDropdownProps {
+ items: (DropdownMenuItem | typeof MENU_SEPARATOR)[]
+ position: Position
+ onItemPress: (item: MenuItem) => void
+ isSubmenu?: boolean
+}
+
+const MenuDropdownComponent = ({ items, position, onItemPress, isSubmenu }: MenuDropdownProps) => {
+ const portalName = useRef(`${isSubmenu ? "submenu" : "dropdown"}-${getUUID()}`).current
+ const { openSubmenu, submenuPosition, handleItemHover } = useSubmenuState(position)
+
+ const isSeparator = (item: MenuItem | typeof MENU_SEPARATOR): item is typeof MENU_SEPARATOR => {
+ return item === MENU_SEPARATOR
+ }
+
+ // Find the submenu item if one is open
+ const submenuItem = openSubmenu
+ ? (items.find((item) => !isSeparator(item) && item.label === openSubmenu) as
+ | DropdownMenuItem
+ | undefined)
+ : undefined
+
+ const dropdownContent = useMemo(
+ () => (
+
+ {items.map((item, index) => {
+ if (isSeparator(item)) return
+
+ return (
+
+ )
+ })}
+
+ ),
+ [items, isSubmenu, position.x, position.y, onItemPress, handleItemHover],
+ )
+
+ return (
+ <>
+ {dropdownContent}
+ {/* Render submenu */}
+ {submenuItem?.submenu && (
+
+ )}
+ >
+ )
+}
+
+export const MenuDropdown = memo(MenuDropdownComponent)
+
+const $dropdown = themed(({ colors, spacing }) => ({
+ position: "absolute",
+ backgroundColor: colors.cardBackground,
+ borderColor: colors.keyline,
+ borderWidth: 1,
+ borderRadius: 4,
+ minWidth: menuSettings.dropdownMinWidth,
+ paddingVertical: spacing.xs,
+ zIndex: menuSettings.zIndex.dropdown,
+}))
+
+const $submenuDropdown = themed(({ colors, spacing }) => ({
+ position: "absolute",
+ backgroundColor: colors.cardBackground,
+ borderColor: colors.keyline,
+ borderWidth: 1,
+ borderRadius: 4,
+ minWidth: menuSettings.submenuMinWidth,
+ paddingVertical: spacing.xs,
+ zIndex: menuSettings.zIndex.submenu,
+}))
+
+const $menuPosition = (position: Position, isSubmenu: boolean | undefined) => ({
+ left: position.x,
+ top: position.y,
+ zIndex: isSubmenu ? 10001 : 10000,
+})
diff --git a/app/components/Menu/MenuDropdownItem.tsx b/app/components/Menu/MenuDropdownItem.tsx
new file mode 100644
index 0000000..e804b5b
--- /dev/null
+++ b/app/components/Menu/MenuDropdownItem.tsx
@@ -0,0 +1,131 @@
+import { Pressable, Text, View, type ViewStyle, type TextStyle } from "react-native"
+import { useState, useRef, memo, useCallback } from "react"
+import { themed } from "../../theme/theme"
+import { menuSettings } from "./menuSettings"
+import type { MenuItem } from "./types"
+
+interface MenuDropdownItemProps {
+ item: MenuItem
+ index: number
+ onItemPress: (item: MenuItem) => void
+ onItemHover: (itemLabel: string, index: number, hasSubmenu: boolean) => void
+}
+
+const MenuDropdownItemComponent = ({
+ item,
+ index,
+ onItemPress,
+ onItemHover,
+}: MenuDropdownItemProps) => {
+ const [hoveredItem, setHoveredItem] = useState(null)
+ const hoverTimeoutRef = useRef(null)
+ const enabled = item.enabled !== false
+
+ const handleHoverIn = useCallback(() => {
+ // Clear any pending hover clear
+ if (hoverTimeoutRef.current) {
+ clearTimeout(hoverTimeoutRef.current)
+ hoverTimeoutRef.current = null
+ }
+ setHoveredItem(item.label)
+ const hasSubmenu = !!item.submenu
+ onItemHover(item.label, index, hasSubmenu)
+ }, [item.label, item.submenu, index, onItemHover])
+
+ const handleHoverOut = useCallback(() => {
+ // Use a small timeout to prevent flickering between items
+ hoverTimeoutRef.current = setTimeout(() => {
+ setHoveredItem((current) => (current === item.label ? null : current))
+ }, 10)
+ }, [item.label])
+
+ const handlePress = useCallback(() => {
+ if (!item.action || !enabled) return
+ item.action()
+ onItemPress(item)
+ }, [item, onItemPress])
+
+ return (
+ [
+ $dropdownItem(),
+ (pressed || hoveredItem === item.label) && enabled && $dropdownItemHovered(),
+ !enabled && $dropdownItemDisabled,
+ ]}
+ >
+
+ {item.label}
+
+
+ {item.shortcut && (
+
+ {formatShortcut(item.shortcut.windows || "")}
+
+ )}
+ {item.submenu && (
+ ▶
+ )}
+
+
+ )
+}
+
+export const MenuDropdownItem = memo(MenuDropdownItemComponent)
+
+function formatShortcut(shortcut: string): string {
+ if (!shortcut) return ""
+ return shortcut
+ .replace(/cmd/gi, "Ctrl")
+ .replace(/shift/gi, "Shift")
+ .split("+")
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
+ .join("+")
+}
+
+const $dropdownItem = themed(({ spacing }) => ({
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ paddingHorizontal: spacing.sm,
+ paddingVertical: spacing.xs,
+ borderRadius: 4,
+ minHeight: menuSettings.itemMinHeight,
+}))
+
+const $dropdownItemHovered = themed(({ colors }) => ({
+ backgroundColor: colors.neutralVery,
+}))
+
+const $dropdownItemDisabled = {
+ opacity: 0.5,
+}
+
+const $dropdownItemText = themed(({ colors, typography }) => ({
+ color: colors.mainText,
+ fontSize: typography.caption,
+}))
+
+const $dropdownItemTextDisabled = themed((theme) => ({
+ color: theme.colors.neutral,
+}))
+
+const $shortcut = themed(({ colors, typography, spacing }) => ({
+ color: colors.neutral,
+ fontSize: typography.small,
+ marginLeft: spacing.md,
+}))
+
+const $submenuArrow = themed(({ colors, typography, spacing }) => ({
+ color: colors.neutral,
+ fontSize: typography.small,
+ marginLeft: spacing.sm,
+}))
+
+const $rightContent: ViewStyle = {
+ flexDirection: "row",
+ alignItems: "center",
+}
diff --git a/app/components/Menu/MenuOverlay.tsx b/app/components/Menu/MenuOverlay.tsx
new file mode 100644
index 0000000..cf2b967
--- /dev/null
+++ b/app/components/Menu/MenuOverlay.tsx
@@ -0,0 +1,46 @@
+import { Pressable, type ViewStyle } from "react-native"
+import { Portal } from "../Portal"
+import { menuSettings } from "./menuSettings"
+
+interface MenuOverlayProps {
+ onPress: () => void
+ portalName?: string
+ style?: ViewStyle
+ excludeArea?: {
+ top?: number
+ left?: number
+ right?: number
+ bottom?: number
+ }
+}
+
+export const MenuOverlay = ({
+ onPress,
+ portalName = "menu-overlay",
+ style,
+ excludeArea,
+}: MenuOverlayProps) => {
+ return (
+
+
+
+ )
+}
+
+interface OverlayStyleArgs {
+ excludeArea?: { top?: number; left?: number; right?: number; bottom?: number }
+ style?: ViewStyle
+}
+
+const overlayStyle: (args: OverlayStyleArgs) => ViewStyle = ({
+ excludeArea,
+ style,
+}: OverlayStyleArgs) => ({
+ position: "absolute",
+ top: excludeArea?.top ?? 0,
+ left: excludeArea?.left ?? 0,
+ right: excludeArea?.right ?? 0,
+ bottom: excludeArea?.bottom ?? 0,
+ zIndex: menuSettings.zIndex.menuOverlay,
+ ...style,
+})
diff --git a/app/components/Menu/menuSettings.ts b/app/components/Menu/menuSettings.ts
new file mode 100644
index 0000000..abe0a90
--- /dev/null
+++ b/app/components/Menu/menuSettings.ts
@@ -0,0 +1,13 @@
+export const menuSettings = {
+ dropdownMinWidth: 200,
+ submenuMinWidth: 150,
+ itemMinHeight: 28,
+ itemHeight: 32,
+ submenuOffsetX: 200,
+ submenuOffsetY: -5,
+ zIndex: {
+ menuOverlay: 9999,
+ dropdown: 10000,
+ submenu: 10001,
+ },
+} as const
diff --git a/app/components/Menu/types.ts b/app/components/Menu/types.ts
new file mode 100644
index 0000000..0b02bed
--- /dev/null
+++ b/app/components/Menu/types.ts
@@ -0,0 +1,21 @@
+import { PlatformShortcut } from "app/utils/useSystemMenu/types"
+
+export interface Position {
+ x: number
+ y: number
+}
+
+// Generic menu item interface for UI components
+export interface MenuItem {
+ label: string
+ shortcut?: PlatformShortcut
+ enabled?: boolean
+ action?: () => void
+ submenu?: (MenuItem | typeof MENU_SEPARATOR)[]
+}
+
+// Type alias for dropdown menu items (same as MenuItem)
+export type DropdownMenuItem = MenuItem
+
+// Menu separator constant
+export const MENU_SEPARATOR = "menu-item-separator" as const
diff --git a/app/components/Menu/useMenuPositioning.ts b/app/components/Menu/useMenuPositioning.ts
new file mode 100644
index 0000000..150c5ff
--- /dev/null
+++ b/app/components/Menu/useMenuPositioning.ts
@@ -0,0 +1,54 @@
+import { useCallback } from "react"
+import { menuSettings } from "./menuSettings"
+import type { Position } from "./types"
+
+export interface PositioningStrategy {
+ calculateSubmenuPosition: (
+ basePosition: Position,
+ itemIndex: number,
+ parentWidth?: number,
+ ) => Position
+ calculateContextMenuPosition?: (
+ clickPosition: Position,
+ menuSize?: { width: number; height: number },
+ screenSize?: { width: number; height: number },
+ ) => Position
+}
+
+const defaultStrategy: PositioningStrategy = {
+ calculateSubmenuPosition: (
+ basePosition,
+ itemIndex,
+ parentWidth = menuSettings.submenuOffsetX,
+ ) => ({
+ x: basePosition.x + parentWidth,
+ y: basePosition.y + itemIndex * menuSettings.itemHeight + menuSettings.submenuOffsetY,
+ }),
+
+ calculateContextMenuPosition: (clickPosition: Position) => {
+ // Basic positioning - can be enhanced for screen edge detection
+ return {
+ x: clickPosition.x,
+ y: clickPosition.y,
+ }
+ },
+}
+
+export const useMenuPositioning = (strategy: PositioningStrategy = defaultStrategy) => {
+ const calculateSubmenuPosition = useCallback(
+ (basePosition: Position, itemIndex: number, parentWidth?: number) =>
+ strategy.calculateSubmenuPosition(basePosition, itemIndex, parentWidth),
+ [strategy],
+ )
+
+ const calculateContextMenuPosition = useCallback(
+ (clickPosition: Position) =>
+ strategy.calculateContextMenuPosition?.(clickPosition) ?? clickPosition,
+ [strategy],
+ )
+
+ return {
+ calculateSubmenuPosition,
+ calculateContextMenuPosition,
+ }
+}
diff --git a/app/components/Menu/useSubmenuState.ts b/app/components/Menu/useSubmenuState.ts
new file mode 100644
index 0000000..4cb5778
--- /dev/null
+++ b/app/components/Menu/useSubmenuState.ts
@@ -0,0 +1,43 @@
+import { useState, useCallback } from "react"
+import { menuSettings } from "./menuSettings"
+import { type Position } from "./types"
+
+export const useSubmenuState = (basePosition: Position) => {
+ const [openSubmenu, setOpenSubmenu] = useState(null)
+ const [submenuPosition, setSubmenuPosition] = useState({ x: 0, y: 0 })
+
+ const openSubmenuAt = useCallback(
+ (itemLabel: string, index: number) => {
+ setOpenSubmenu(itemLabel)
+ setSubmenuPosition({
+ x: basePosition.x + menuSettings.submenuOffsetX,
+ y: basePosition.y + index * menuSettings.itemHeight + menuSettings.submenuOffsetY,
+ })
+ },
+ [basePosition.x, basePosition.y],
+ )
+
+ const closeSubmenu = useCallback(() => {
+ setOpenSubmenu(null)
+ }, [])
+
+ const handleItemHover = useCallback(
+ (itemLabel: string, index: number, hasSubmenu: boolean) => {
+ if (hasSubmenu) {
+ openSubmenuAt(itemLabel, index)
+ } else {
+ if (openSubmenu) {
+ closeSubmenu()
+ }
+ }
+ },
+ [openSubmenu, openSubmenuAt, closeSubmenu],
+ )
+
+ return {
+ openSubmenu,
+ submenuPosition,
+ handleItemHover,
+ closeSubmenu,
+ }
+}
diff --git a/app/components/SystemMenu.tsx b/app/components/SystemMenu.tsx
new file mode 100644
index 0000000..b2b0bae
--- /dev/null
+++ b/app/components/SystemMenu.tsx
@@ -0,0 +1,105 @@
+import { useMemo, useState } from "react"
+import { DevSettings, NativeModules } from "react-native"
+import { useSidebar } from "../state/useSidebar"
+import { useGlobal, withGlobal } from "../state/useGlobal"
+import { useSystemMenu } from "../utils/useSystemMenu/useSystemMenu"
+import { TimelineItem } from "app/types"
+import { MenuItemId } from "./Sidebar/SidebarMenu"
+import { AboutModal } from "./AboutModal"
+
+export function SystemMenu({ children }: { children: React.ReactNode }) {
+ const { toggleSidebar } = useSidebar()
+ const [_, setActiveItem] = useGlobal("sidebar-active-item", "logs", {
+ persist: true,
+ })
+ const [__, setTimelineItems] = withGlobal("timelineItems", [], {
+ persist: true,
+ })
+ const [aboutVisible, setAboutVisible] = useState(false)
+
+ const menuConfig = useMemo(
+ () => ({
+ remove: ["File", "Edit", "Format", "Reactotron > About Reactotron"],
+ items: {
+ Reactotron: [
+ {
+ label: "About Reactotron",
+ position: 0,
+ action: () => setAboutVisible(true),
+ },
+ ],
+ View: [
+ {
+ label: "Toggle Sidebar",
+ shortcut: { macos: "cmd+b", windows: "ctrl+b" },
+ action: toggleSidebar,
+ },
+ {
+ label: "Logs Tab",
+ shortcut: { macos: "cmd+1", windows: "ctrl+1" },
+ action: () => setActiveItem("logs"),
+ },
+ {
+ label: "Network Tab",
+ shortcut: { macos: "cmd+2", windows: "ctrl+2" },
+ action: () => setActiveItem("network"),
+ },
+ {
+ label: "Performance Tab",
+ shortcut: { macos: "cmd+3", windows: "ctrl+3" },
+ action: () => setActiveItem("performance"),
+ },
+ {
+ label: "Plugins Tab",
+ shortcut: { macos: "cmd+4", windows: "ctrl+4" },
+ action: () => setActiveItem("plugins"),
+ },
+ {
+ label: "Custom Commands Tab",
+ shortcut: { macos: "cmd+5", windows: "ctrl+5" },
+ action: () => setActiveItem("customCommands"),
+ },
+ {
+ label: "Help Tab",
+ shortcut: { macos: "cmd+6", windows: "ctrl+6" },
+ action: () => setActiveItem("help"),
+ },
+ ...(__DEV__
+ ? [
+ {
+ label: "Toggle Dev Menu",
+ shortcut: { macos: "cmd+shift+d", windows: "ctrl+shift+d" },
+ action: () => NativeModules.DevMenu.show(),
+ },
+ ]
+ : []),
+ ],
+ Window: [
+ {
+ label: "Reload",
+ shortcut: { macos: "cmd+shift+r", windows: "ctrl+shift+r" },
+ action: () => DevSettings.reload(),
+ },
+ ],
+ Tools: [
+ {
+ label: "Clear Timeline Items",
+ shortcut: { macos: "cmd+k", windows: "ctrl+k" },
+ action: () => setTimelineItems([]),
+ },
+ ],
+ },
+ }),
+ [toggleSidebar, setActiveItem],
+ )
+
+ useSystemMenu(menuConfig)
+
+ return (
+ <>
+ {children}
+
+ setAboutVisible(false)} />
+ >
+ )
+}
diff --git a/app/components/Titlebar/Titlebar.tsx b/app/components/Titlebar/Titlebar.tsx
index 97c473b..b44b671 100644
--- a/app/components/Titlebar/Titlebar.tsx
+++ b/app/components/Titlebar/Titlebar.tsx
@@ -4,6 +4,7 @@ import { Icon } from "../Icon"
import ActionButton from "../ActionButton"
import { useSidebar } from "../../state/useSidebar"
import { PassthroughView } from "./PassthroughView"
+import { TitlebarMenu } from "./TitlebarMenu"
import { useGlobal } from "../../state/useGlobal"
import { ClientTab } from "../ClientTab"
@@ -16,6 +17,11 @@ export const Titlebar = () => {
+ {Platform.OS === "windows" && (
+
+
+
+ )}
(
diff --git a/app/components/Titlebar/TitlebarMenu.tsx b/app/components/Titlebar/TitlebarMenu.tsx
new file mode 100644
index 0000000..6b729a2
--- /dev/null
+++ b/app/components/Titlebar/TitlebarMenu.tsx
@@ -0,0 +1,78 @@
+import { View, ViewStyle } from "react-native"
+import { useState, useCallback, useRef } from "react"
+import { themed } from "../../theme/theme"
+import { TitlebarMenuItem } from "./TitlebarMenuItem"
+import { MenuDropdown } from "../Menu/MenuDropdown"
+import { MenuOverlay } from "../Menu/MenuOverlay"
+import type { DropdownMenuItem, Position } from "../Menu/types"
+import { PassthroughView } from "./PassthroughView"
+import { useSystemMenu } from "../../utils/useSystemMenu/useSystemMenu"
+
+export const TitlebarMenu = () => {
+ const { menuStructure, menuItems, handleMenuItemPressed } = useSystemMenu()
+ const [openMenu, setOpenMenu] = useState(null)
+ const [dropdownPosition, setDropdownPosition] = useState({ x: 0, y: 0 })
+ const menuRefs = useRef