{
  "id": "app",
  "name": "Expo App (server, db, user)",
  "description": "Expo Router mobile + Express/TRPC/Drizzle backend with Manus OAuth. Ships web preview + QR friendly Expo dev server scripts so webdev_init_project can satisfy \"build an app\" requests.",
  "capabilities": [
    "server",
    "db",
    "user",
    "mobile",
    "app"
  ],
  "files": {
    "components/screen-container.tsx": "import { View, type ViewProps } from \"react-native\";\nimport { SafeAreaView, type Edge } from \"react-native-safe-area-context\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface ScreenContainerProps extends ViewProps {\n  /**\n   * SafeArea edges to apply. Defaults to [\"top\", \"left\", \"right\"].\n   * Bottom is typically handled by Tab Bar.\n   */\n  edges?: Edge[];\n  /**\n   * Tailwind className for the content area.\n   */\n  className?: string;\n  /**\n   * Additional className for the outer container (background layer).\n   */\n  containerClassName?: string;\n  /**\n   * Additional className for the SafeAreaView (content layer).\n   */\n  safeAreaClassName?: string;\n}\n\n/**\n * A container component that properly handles SafeArea and background colors.\n *\n * The outer View extends to full screen (including status bar area) with the background color,\n * while the inner SafeAreaView ensures content is within safe bounds.\n *\n * Usage:\n * ```tsx\n * <ScreenContainer className=\"p-4\">\n *   <Text className=\"text-2xl font-bold text-foreground\">\n *     Welcome\n *   </Text>\n * </ScreenContainer>\n * ```\n */\nexport function ScreenContainer({\n  children,\n  edges = [\"top\", \"left\", \"right\"],\n  className,\n  containerClassName,\n  safeAreaClassName,\n  style,\n  ...props\n}: ScreenContainerProps) {\n  return (\n    <View\n      className={cn(\n        \"flex-1\",\n        \"bg-background\",\n        containerClassName\n      )}\n      {...props}\n    >\n      <SafeAreaView\n        edges={edges}\n        className={cn(\"flex-1\", safeAreaClassName)}\n        style={style}\n      >\n        <View className={cn(\"flex-1\", className)}>{children}</View>\n      </SafeAreaView>\n    </View>\n  );\n}",
    "app/(tabs)/_layout.tsx": "import { Tabs } from \"expo-router\";\nimport { useSafeAreaInsets } from \"react-native-safe-area-context\";\n\nimport { HapticTab } from \"@/components/haptic-tab\";\nimport { IconSymbol } from \"@/components/ui/icon-symbol\";\nimport { Platform } from \"react-native\";\nimport { useColors } from \"@/hooks/use-colors\";\n\nexport default function TabLayout() {\n  const colors = useColors();\n  const insets = useSafeAreaInsets();\n  const bottomPadding = Platform.OS === \"web\" ? 12 : Math.max(insets.bottom, 8);\n  const tabBarHeight = 56 + bottomPadding;\n\n  return (\n    <Tabs\n      screenOptions={{\n        tabBarActiveTintColor: colors.tint,\n        headerShown: false,\n        tabBarButton: HapticTab,\n        tabBarStyle: {\n          paddingTop: 8,\n          paddingBottom: bottomPadding,\n          height: tabBarHeight,\n          backgroundColor: colors.background,\n          borderTopColor: colors.border,\n          borderTopWidth: 0.5,\n        },\n      }}\n    >\n      <Tabs.Screen\n        name=\"index\"\n        options={{\n          title: \"Home\",\n          tabBarIcon: ({ color }) => <IconSymbol size={28} name=\"house.fill\" color={color} />,\n        }}\n      />\n    </Tabs>\n  );\n}",
    "app/(tabs)/index.tsx": "import { ScrollView, Text, View, TouchableOpacity } from \"react-native\";\n\nimport { ScreenContainer } from \"@/components/screen-container\";\n\n/**\n * Home Screen - NativeWind Example\n *\n * This template uses NativeWind (Tailwind CSS for React Native).\n * You can use familiar Tailwind classes directly in className props.\n *\n * Key patterns:\n * - Use `className` instead of `style` for most styling\n * - Theme colors: use tokens directly (bg-background, text-foreground, bg-primary, etc.); no dark: prefix needed\n * - Responsive: standard Tailwind breakpoints work on web\n * - Custom colors defined in tailwind.config.js\n */\nexport default function HomeScreen() {\n  return (\n    <ScreenContainer className=\"p-6\">\n      <ScrollView contentContainerStyle={{ flexGrow: 1 }}>\n        <View className=\"flex-1 gap-8\">\n          {/* Hero Section */}\n          <View className=\"items-center gap-2\">\n            <Text className=\"text-4xl font-bold text-foreground\">Welcome</Text>\n            <Text className=\"text-base text-muted text-center\">\n              Edit app/(tabs)/index.tsx to get started\n            </Text>\n          </View>\n\n          {/* Example Card */}\n          <View className=\"w-full max-w-sm self-center bg-surface rounded-2xl p-6 shadow-sm border border-border\">\n            <Text className=\"text-lg font-semibold text-foreground mb-2\">NativeWind Ready</Text>\n            <Text className=\"text-sm text-muted leading-relaxed\">\n              Use Tailwind CSS classes directly in your React Native components.\n            </Text>\n          </View>\n\n          {/* Example Button */}\n          <View className=\"items-center\">\n            <TouchableOpacity className=\"bg-primary px-6 py-3 rounded-full active:opacity-80\">\n              <Text className=\"text-background font-semibold\">Get Started</Text>\n            </TouchableOpacity>\n          </View>\n        </View>\n      </ScrollView>\n    </ScreenContainer>\n  );\n}",
    "lib/utils.ts": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Combines class names using clsx and tailwind-merge.\n * This ensures Tailwind classes are properly merged without conflicts.\n *\n * Usage:\n * ```tsx\n * cn(\"px-4 py-2\", isActive && \"bg-primary\", className)\n * ```\n */\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}",
    "hooks/use-colors.ts": "import { Colors, type ColorScheme, type ThemeColorPalette } from \"@/constants/theme\";\nimport { useColorScheme } from \"./use-color-scheme\";\n\n/**\n * Returns the current theme's color palette.\n * Usage: const colors = useColors(); then colors.text, colors.background, etc.\n */\nexport function useColors(colorSchemeOverride?: ColorScheme): ThemeColorPalette {\n  const colorSchema = useColorScheme();\n  const scheme = (colorSchemeOverride ?? colorSchema ?? \"light\") as ColorScheme;\n  return Colors[scheme];\n}",
    "components/ui/icon-symbol.tsx": "// Fallback for using MaterialIcons on Android and web.\n\nimport MaterialIcons from \"@expo/vector-icons/MaterialIcons\";\nimport { SymbolWeight, SymbolViewProps } from \"expo-symbols\";\nimport { ComponentProps } from \"react\";\nimport { OpaqueColorValue, type StyleProp, type TextStyle } from \"react-native\";\n\ntype IconMapping = Record<SymbolViewProps[\"name\"], ComponentProps<typeof MaterialIcons>[\"name\"]>;\ntype IconSymbolName = keyof typeof MAPPING;\n\n/**\n * Add your SF Symbols to Material Icons mappings here.\n * - see Material Icons in the [Icons Directory](https://icons.expo.fyi).\n * - see SF Symbols in the [SF Symbols](https://developer.apple.com/sf-symbols/) app.\n */\nconst MAPPING = {\n  \"house.fill\": \"home\",\n  \"paperplane.fill\": \"send\",\n  \"chevron.left.forwardslash.chevron.right\": \"code\",\n  \"chevron.right\": \"chevron-right\",\n} as IconMapping;\n\n/**\n * An icon component that uses native SF Symbols on iOS, and Material Icons on Android and web.\n * This ensures a consistent look across platforms, and optimal resource usage.\n * Icon `name`s are based on SF Symbols and require manual mapping to Material Icons.\n */\nexport function IconSymbol({\n  name,\n  size = 24,\n  color,\n  style,\n}: {\n  name: IconSymbolName;\n  size?: number;\n  color: string | OpaqueColorValue;\n  style?: StyleProp<TextStyle>;\n  weight?: SymbolWeight;\n}) {\n  return <MaterialIcons color={color} size={size} name={MAPPING[name]} style={style} />;\n}",
    "tailwind.config.js": "const { themeColors } = require(\"./theme.config\");\nconst plugin = require(\"tailwindcss/plugin\");\n\nconst tailwindColors = Object.fromEntries(\n  Object.entries(themeColors).map(([name, swatch]) => [\n    name,\n    {\n      DEFAULT: `var(--color-${name})`,\n      light: swatch.light,\n      dark: swatch.dark,\n    },\n  ]),\n);\n\n/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  darkMode: \"class\",\n  // Scan all component and app files for Tailwind classes\n  content: [\"./app/**/*.{js,ts,tsx}\", \"./components/**/*.{js,ts,tsx}\", \"./lib/**/*.{js,ts,tsx}\", \"./hooks/**/*.{js,ts,tsx}\"],\n\n  presets: [require(\"nativewind/preset\")],\n  theme: {\n    extend: {\n      colors: tailwindColors,\n    },\n  },\n  plugins: [\n    plugin(({ addVariant }) => {\n      addVariant(\"light\", ':root:not([data-theme=\"dark\"]) &');\n      addVariant(\"dark\", ':root[data-theme=\"dark\"] &');\n    }),\n  ],\n};",
    "theme.config.js": "/** @type {const} */\nconst themeColors = {\n  primary: { light: '#0a7ea4', dark: '#0a7ea4' },\n  background: { light: '#ffffff', dark: '#151718' },\n  surface: { light: '#f5f5f5', dark: '#1e2022' },\n  foreground: { light: '#11181C', dark: '#ECEDEE' },\n  muted: { light: '#687076', dark: '#9BA1A6' },\n  border: { light: '#E5E7EB', dark: '#334155' },\n  success: { light: '#22C55E', dark: '#4ADE80' },\n  warning: { light: '#F59E0B', dark: '#FBBF24' },\n  error: { light: '#EF4444', dark: '#F87171' },\n};\n\nmodule.exports = { themeColors };",
    "app.config.ts": "// Load environment variables with proper priority (system > .env)\nimport \"./scripts/load-env.js\";\nimport type { ExpoConfig } from \"expo/config\";\n\n// Bundle ID format: space.manus.<project_name_dots>.<timestamp>\n// e.g., \"my-app\" created at 2024-01-15 10:30:45 -> \"space.manus.my.app.t20240115103045\"\n// Bundle ID can only contain letters, numbers, and dots\n// Android requires each dot-separated segment to start with a letter\nconst rawBundleId = \"com.app.iptvplayerios\";\nconst bundleId =\n  rawBundleId\n    .replace(/[-_]/g, \".\") // Replace hyphens/underscores with dots\n    .replace(/[^a-zA-Z0-9.]/g, \"\") // Remove invalid chars\n    .replace(/\\.+/g, \".\") // Collapse consecutive dots\n    .replace(/^\\.+|\\.+$/g, \"\") // Trim leading/trailing dots\n    .toLowerCase()\n    .split(\".\")\n    .map((segment) => {\n      // Android requires each segment to start with a letter\n      // Prefix with 'x' if segment starts with a digit\n      return /^[a-zA-Z]/.test(segment) ? segment : \"x\" + segment;\n    })\n    .join(\".\") || \"space.manus.app\";\n// Extract timestamp from bundle ID and prefix with \"manus\" for deep link scheme\n// e.g., \"space.manus.my.app.t20240115103045\" -> \"manus20240115103045\"\nconst timestamp = bundleId.split(\".\").pop()?.replace(/^t/, \"\") ?? \"\";\nconst schemeFromBundleId = `manus${timestamp}`;\n\nconst env = {\n  // App branding - update these values directly (do not use env vars)\n  appName: \"Manus IPTV Player\",\n  appSlug: \"iptv-player-ios\",\n  // S3 URL of the app logo - set this to the URL returned by generate_image when creating custom logo\n  // Leave empty to use the default icon from assets/images/icon.png\n  logoUrl: \"\",\n  scheme: schemeFromBundleId,\n  iosBundleId: bundleId,\n  androidPackage: bundleId,\n};\n\nconst config: ExpoConfig = {\n  name: env.appName,\n  slug: env.appSlug,\n  version: \"1.0.0\",\n  orientation: \"portrait\",\n  icon: \"./assets/images/icon.png\",\n  scheme: env.scheme,\n  userInterfaceStyle: \"automatic\",\n  newArchEnabled: true,\n  ios: {\n    supportsTablet: true,\n    bundleIdentifier: env.iosBundleId,\n    \"infoPlist\": {\n        \"ITSAppUsesNonExemptEncryption\": false\n      }\n  },\n  android: {\n    adaptiveIcon: {\n      backgroundColor: \"#E6F4FE\",\n      foregroundImage: \"./assets/images/android-icon-foreground.png\",\n      backgroundImage: \"./assets/images/android-icon-background.png\",\n      monochromeImage: \"./assets/images/android-icon-monochrome.png\",\n    },\n    edgeToEdgeEnabled: true,\n    predictiveBackGestureEnabled: false,\n    package: env.androidPackage,\n    permissions: [\"POST_NOTIFICATIONS\"],\n    intentFilters: [\n      {\n        action: \"VIEW\",\n        autoVerify: true,\n        data: [\n          {\n            scheme: env.scheme,\n            host: \"*\",\n          },\n        ],\n        category: [\"BROWSABLE\", \"DEFAULT\"],\n      },\n    ],\n  },\n  web: {\n    bundler: \"metro\",\n    output: \"static\",\n    favicon: \"./assets/images/favicon.png\",\n  },\n  plugins: [\n    \"expo-router\",\n    [\n      \"expo-audio\",\n      {\n        microphonePermission: \"Allow $(PRODUCT_NAME) to access your microphone.\",\n      },\n    ],\n    [\n      \"expo-video\",\n      {\n        supportsBackgroundPlayback: true,\n        supportsPictureInPicture: true,\n      },\n    ],\n    [\n      \"expo-splash-screen\",\n      {\n        image: \"./assets/images/splash-icon.png\",\n        imageWidth: 200,\n        resizeMode: \"contain\",\n        backgroundColor: \"#ffffff\",\n        dark: {\n          backgroundColor: \"#000000\",\n        },\n      },\n    ],\n    [\n      \"expo-build-properties\",\n      {\n        android: {\n          buildArchs: [\"armeabi-v7a\", \"arm64-v8a\"],\n          minSdkVersion: 24,\n        },\n      },\n    ],\n  ],\n  experiments: {\n    typedRoutes: true,\n    reactCompiler: true,\n  },\n};\n\nexport default config;",
    "package.json": "{\n  \"name\": \"app-template\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"main\": \"expo-router/entry\",\n  \"scripts\": {\n    \"dev\": \"concurrently -k \\\"pnpm dev:server\\\" \\\"pnpm dev:metro\\\"\",\n    \"dev:server\": \"cross-env NODE_ENV=development tsx watch server/_core/index.ts\",\n    \"dev:metro\": \"cross-env EXPO_USE_METRO_WORKSPACE_ROOT=1 npx expo start --web --port ${EXPO_PORT:-8081}\",\n    \"build\": \"esbuild server/_core/index.ts --platform=node --packages=external --bundle --format=esm --outdir=dist\",\n    \"start\": \"NODE_ENV=production node dist/index.js\",\n    \"check\": \"tsc --noEmit\",\n    \"lint\": \"expo lint\",\n    \"format\": \"prettier --write .\",\n    \"test\": \"vitest run\",\n    \"db:push\": \"drizzle-kit generate && drizzle-kit migrate\",\n    \"android\": \"expo start --android\",\n    \"ios\": \"expo start --ios\",\n    \"qr\": \"node scripts/generate_qr.mjs\"\n  },\n  \"dependencies\": {\n    \"@expo/vector-icons\": \"^15.0.3\",\n    \"@react-native-async-storage/async-storage\": \"^2.2.0\",\n    \"@react-navigation/bottom-tabs\": \"^7.8.12\",\n    \"@react-navigation/elements\": \"^2.9.2\",\n    \"@react-navigation/native\": \"^7.1.25\",\n    \"@tanstack/react-query\": \"^5.90.12\",\n    \"@trpc/client\": \"11.7.2\",\n    \"@trpc/react-query\": \"11.7.2\",\n    \"@trpc/server\": \"11.7.2\",\n    \"axios\": \"^1.13.2\",\n    \"clsx\": \"^2.1.1\",\n    \"cookie\": \"^1.1.1\",\n    \"dotenv\": \"^16.6.1\",\n    \"drizzle-orm\": \"^0.44.7\",\n    \"expo\": \"~54.0.29\",\n    \"expo-audio\": \"~1.1.0\",\n    \"expo-build-properties\": \"^1.0.10\",\n    \"expo-constants\": \"~18.0.12\",\n    \"expo-font\": \"~14.0.10\",\n    \"expo-haptics\": \"~15.0.8\",\n    \"expo-image\": \"~3.0.11\",\n    \"expo-keep-awake\": \"~15.0.8\",\n    \"expo-linking\": \"~8.0.10\",\n    \"expo-notifications\": \"~0.32.15\",\n    \"expo-router\": \"~6.0.19\",\n    \"expo-secure-store\": \"~15.0.8\",\n    \"expo-splash-screen\": \"~31.0.12\",\n    \"expo-status-bar\": \"~3.0.9\",\n    \"expo-symbols\": \"~1.0.8\",\n    \"expo-system-ui\": \"~6.0.9\",\n    \"expo-video\": \"~3.0.15\",\n    \"expo-web-browser\": \"~15.0.10\",\n    \"express\": \"^4.22.1\",\n    \"jose\": \"6.1.0\",\n    \"mysql2\": \"^3.16.0\",\n    \"nativewind\": \"^4.2.1\",\n    \"react\": \"19.1.0\",\n    \"react-dom\": \"19.1.0\",\n    \"react-native\": \"0.81.5\",\n    \"react-native-gesture-handler\": \"~2.28.0\",\n    \"react-native-reanimated\": \"~4.1.6\",\n    \"react-native-safe-area-context\": \"~5.6.2\",\n    \"react-native-screens\": \"~4.16.0\",\n    \"react-native-svg\": \"15.12.1\",\n    \"react-native-web\": \"~0.21.2\",\n    \"react-native-worklets\": \"0.5.1\",\n    \"superjson\": \"^1.13.3\",\n    \"tailwind-merge\": \"^2.6.0\",\n    \"zod\": \"^4.2.1\"\n  },\n  \"devDependencies\": {\n    \"@expo/ngrok\": \"^4.1.3\",\n    \"@types/cookie\": \"^0.6.0\",\n    \"@types/express\": \"^4.17.25\",\n    \"@types/node\": \"^22.19.3\",\n    \"@types/qrcode\": \"^1.5.6\",\n    \"@types/react\": \"~19.1.17\",\n    \"concurrently\": \"^9.2.1\",\n    \"cross-env\": \"^7.0.3\",\n    \"drizzle-kit\": \"^0.31.8\",\n    \"esbuild\": \"^0.25.12\",\n    \"eslint\": \"^9.39.2\",\n    \"eslint-config-expo\": \"~10.0.0\",\n    \"prettier\": \"^3.7.4\",\n    \"qrcode\": \"^1.5.4\",\n    \"tailwindcss\": \"^3.4.17\",\n    \"tsx\": \"^4.21.0\",\n    \"typescript\": \"~5.9.3\",\n    \"vitest\": \"^2.1.9\"\n  },\n  \"packageManager\": \"pnpm@9.12.0\"\n}"
  }
}
