{"_id":"@codyzk/onboarding","_rev":"3-c168c2add203cbc816b997e1a5e4c8f5","name":"@codyzk/onboarding","dist-tags":{"latest":"0.0.3"},"versions":{"0.0.1":{"name":"@codyzk/onboarding","version":"0.0.1","_id":"@codyzk/onboarding@0.0.1","maintainers":[{"name":"codyzk","email":"codyzyke@gmail.com"}],"dist":{"shasum":"97c2494f1dc6e084a47d43a9d2a0249cdeac04c2","tarball":"https://registry.npmjs.org/@codyzk/onboarding/-/onboarding-0.0.1.tgz","fileCount":24,"integrity":"sha512-frd86ID8HZy4N4S/3aH8/TqmxpFiMmH9kn3gEgOw7gGJ6/3bDb0QQUtWevp+2lX6LawwxY5nGxqlYDesxkN23w==","signatures":[{"sig":"MEYCIQDw8O0/uAtS6QYnaU1yRuoogR/vJ0/YZaqnK0z143QI1gIhALFP9EeM+XY5yDtpgh5crmN32yKRvCOVN9/unga0c5qB","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":343300},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"},"./rating":{"types":"./dist/integrations/rating/index.d.ts","import":"./dist/integrations/rating/index.mjs","require":"./dist/integrations/rating/index.js"},"./permissions":{"types":"./dist/permissions/index.d.ts","import":"./dist/permissions/index.mjs","require":"./dist/permissions/index.js"}},"gitHead":"9d8afc0a15ed4010245098b8c486613d8547b409","private":false,"scripts":{"dev":"tsup --watch","test":"node --import tsx --test \"src/**/__tests__/*.test.ts\" \"__tests__/**/*.spec.ts\"","build":"tsup","clean":"rimraf dist || rm -rf dist","typecheck":"tsc -p tsconfig.json --noEmit"},"_npmUser":{"name":"codyzk","email":"codyzyke@gmail.com"},"_npmVersion":"11.6.0","description":"Configurable onboarding flows for React Native apps.","directories":{},"sideEffects":false,"_nodeVersion":"22.17.0","dependencies":{"zod":"^4.1.12"},"react-native":"dist/index.mjs","_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.20.6","@types/node":"^24.9.1"},"peerDependencies":{"react":">=18.2 <20","react-native":">=0.72 <1"},"peerDependenciesMeta":{"react-native":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/onboarding_0.0.1_1761505754916_0.6146851784215386","host":"s3://npm-registry-packages-npm-production"}},"0.0.2":{"name":"@codyzk/onboarding","version":"0.0.2","_id":"@codyzk/onboarding@0.0.2","maintainers":[{"name":"codyzk","email":"codyzyke@gmail.com"}],"dist":{"shasum":"0a88dd22927872df3c5e36baf1d759459abaccfe","tarball":"https://registry.npmjs.org/@codyzk/onboarding/-/onboarding-0.0.2.tgz","fileCount":24,"integrity":"sha512-qu3o7LVw/wkvFX6wtLfAmER6QMGfF10TmMG7LnEiL4WIThNE78iIp3YJEpwIid6Lx6ba+6kbCSMUL76P3pX80g==","signatures":[{"sig":"MEQCIH2L+gl839o85PNPabh+KA9j1ciBiOhc1/Yc1XPx2ilVAiBTahgMkcSLf6BldD1t79wsHKcObEHkubCyQnC2cAIeZA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":343379},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"},"./rating":{"types":"./dist/integrations/rating/index.d.ts","import":"./dist/integrations/rating/index.mjs","require":"./dist/integrations/rating/index.js"},"./permissions":{"types":"./dist/permissions/index.d.ts","import":"./dist/permissions/index.mjs","require":"./dist/permissions/index.js"}},"gitHead":"9d8afc0a15ed4010245098b8c486613d8547b409","private":false,"scripts":{"dev":"tsup --watch","test":"node --import tsx --test \"src/**/__tests__/*.test.ts\" \"__tests__/**/*.spec.ts\"","build":"tsup","clean":"rimraf dist || rm -rf dist","typecheck":"tsc -p tsconfig.json --noEmit"},"_npmUser":{"name":"codyzk","email":"codyzyke@gmail.com"},"_npmVersion":"11.6.0","description":"Configurable onboarding flows for React Native apps.","directories":{},"sideEffects":false,"_nodeVersion":"22.17.0","dependencies":{"zod":"^4.1.12"},"react-native":"dist/index.mjs","_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.20.6","@types/node":"^24.9.1"},"peerDependencies":{"react":">=18.2 <20","react-native":">=0.72 <1"},"peerDependenciesMeta":{"react-native":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/onboarding_0.0.2_1761505862832_0.5334237835286235","host":"s3://npm-registry-packages-npm-production"}},"0.0.3":{"name":"@codyzk/onboarding","version":"0.0.3","private":false,"main":"dist/index.js","module":"dist/index.mjs","types":"dist/index.d.ts","react-native":"dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"},"./permissions":{"types":"./dist/permissions/index.d.ts","import":"./dist/permissions/index.mjs","require":"./dist/permissions/index.js"},"./rating":{"types":"./dist/integrations/rating/index.d.ts","import":"./dist/integrations/rating/index.mjs","require":"./dist/integrations/rating/index.js"}},"sideEffects":false,"scripts":{"build":"tsup","dev":"tsup --watch","clean":"rimraf dist || rm -rf dist","typecheck":"tsc -p tsconfig.json --noEmit","test":"node --import tsx --test \"src/**/__tests__/*.test.ts\" \"__tests__/**/*.spec.ts\""},"peerDependencies":{"react":">=18.2 <20","react-native":">=0.72 <1"},"peerDependenciesMeta":{"react-native":{"optional":true}},"devDependencies":{"@types/node":"^24.9.1","tsx":"^4.20.6"},"dependencies":{"zod":"^4.1.12"},"_id":"@codyzk/onboarding@0.0.3","gitHead":"e7dfd6a4a47a333d47498f705cae335a8bb75b18","description":"**JSON-driven, component-based onboarding flows for React Native apps.**","_nodeVersion":"22.17.0","_npmVersion":"11.6.0","dist":{"integrity":"sha512-kRWFJZ6V8OTsKHcXTqIetzTUwKBIGsT6SxrreBUlrmQAqDXWLYonsYRmzG1u8ln/10jcTxiumXQr3g8WQqNK+A==","shasum":"562f2ba0ec4dcf1525eae0467bf80c02eda7877e","tarball":"https://registry.npmjs.org/@codyzk/onboarding/-/onboarding-0.0.3.tgz","fileCount":24,"unpackedSize":435601,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQD+vmTDCFTXY9bcGPV5Bgwff7+LR5XSxRCPLya9xc84ngIgG7Q88PV1Slo6irAYzh6r7l58lgdTmqmHwA8pdqGGyeI="}]},"_npmUser":{"name":"codyzk","email":"codyzyke@gmail.com"},"directories":{},"maintainers":[{"name":"codyzk","email":"codyzyke@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/onboarding_0.0.3_1761587883835_0.16136294447250488"},"_hasShrinkwrap":false}},"time":{"created":"2025-10-26T19:09:14.842Z","modified":"2025-10-27T17:58:04.274Z","0.0.1":"2025-10-26T19:09:15.122Z","0.0.2":"2025-10-26T19:11:03.025Z","0.0.3":"2025-10-27T17:58:04.052Z"},"description":"**JSON-driven, component-based onboarding flows for React Native apps.**","maintainers":[{"name":"codyzk","email":"codyzyke@gmail.com"}],"readme":"# @codyzk/onboarding\n\n**JSON-driven, component-based onboarding flows for React Native apps.**\n\nBuild flexible, beautiful onboarding experiences with:\n\n- 🎨 **Token-based design system** - Centralized colors, spacing, typography\n- 📦 **Component array system** - Arrange UI elements in any order\n- 🎯 **Type-safe** - Full TypeScript + Zod validation\n- 🔌 **Pluggable** - Custom adapters for permissions and ratings\n- 📱 **Cross-platform** - Works on iOS, Android, and Web\n\n## Installation\n\n```bash\nnpm install @codyzk/onboarding\n\n# or\npnpm add @codyzk/onboarding\n```\n\n## Quick Start\n\n```tsx\nimport { Onboarding } from \"@codyzk/onboarding\";\nimport flow from \"./flows/onboarding.json\";\n\nfunction App() {\n  return (\n    <Onboarding\n      flow={flow}\n      theme=\"auto\" // \"light\", \"dark\", or \"auto\" (follows system)\n      onEnd={(result) => {\n        // Navigate based on result.dest: \"paywall\", \"home\", or \"custom\"\n        if (result.dest === \"paywall\") {\n          navigation.navigate(\"Paywall\");\n        } else {\n          navigation.navigate(\"Home\");\n        }\n      }}\n      analytics={{\n        screen: (node) => {\n          analytics.track(\"Onboarding Screen Viewed\", {\n            nodeId: node.id,\n            type: node.type,\n          });\n        },\n      }}\n    />\n  );\n}\n```\n\n## Component Array System v1\n\nThe new component system provides complete flexibility over your onboarding UI:\n\n```json\n{\n  \"id\": \"welcome_flow\",\n  \"version\": 1,\n  \"start\": \"welcome\",\n  \"nodes\": {\n    \"welcome\": {\n      \"type\": \"screen\",\n      \"id\": \"welcome\",\n      \"schemaVersion\": 1,\n      \"tokens\": {\n        \"colors\": {\n          \"brand\": \"#4F46E5\",\n          \"text\": \"#111827\"\n        },\n        \"spacing\": {\n          \"md\": 16,\n          \"lg\": 24\n        },\n        \"typography\": {\n          \"title\": { \"size\": 28, \"weight\": 700 }\n        }\n      },\n      \"components\": [\n        {\n          \"id\": \"root\",\n          \"type\": \"box\",\n          \"style\": { \"padding\": \"lg\", \"gap\": \"md\" },\n          \"children\": [\n            {\n              \"id\": \"title\",\n              \"type\": \"text\",\n              \"content\": \"Welcome!\",\n              \"typography\": { \"preset\": \"title\" },\n              \"style\": { \"color\": \"brand\" }\n            },\n            {\n              \"id\": \"cta\",\n              \"type\": \"button\",\n              \"label\": \"Get Started\",\n              \"action\": { \"kind\": \"next\", \"to\": \"next_screen\" },\n              \"style\": { \"backgroundColor\": \"brand\" }\n            }\n          ]\n        }\n      ]\n    }\n  }\n}\n```\n\n**Key Features:**\n\n- **Tokens** - Define colors, spacing, radii, typography once\n- **Components** - Text, image, button, box, spacer, divider\n- **Flexible layouts** - Nested boxes with flexbox styling\n- **Actions** - Button actions for navigation, links, events\n\n📖 **[Read the Implementation Guide](./IMPLEMENTATION.md)** for detailed documentation.\n\n## API Reference\n\n### `<Onboarding />` Props\n\n| Prop                | Type                                                   | Required             | Default           | Description                                                                                  |\n| ------------------- | ------------------------------------------------------ | -------------------- | ----------------- | -------------------------------------------------------------------------------------------- |\n| `flow`              | `Flow` \\| `Record<string, unknown>`                    | One of flow/flowPath | -                 | Inline flow object (e.g., `import flow from \"./flows/default.json\"`)                         |\n| `flowPath`          | `string`                                               | One of flow/flowPath | -                 | Path to a flow JSON file. Useful in Node/web builds where dynamic `require` is available.    |\n| `onEnd`             | `(result) => void`                                     | No                   | -                 | Callback when flow ends. Receives `{ dest: \"home\" \\| \"paywall\" \\| \"custom\", payload?: any }` |\n| `theme`             | `\"light\" \\| \"dark\" \\| \"auto\"`                          | No                   | `\"auto\"`          | Theme mode. \"auto\" follows system appearance                                                 |\n| `permissionAdapter` | `PermissionAdapter`                                    | No                   | Stub              | Custom permission adapter (default uses in-memory stub)                                      |\n| `ratingAdapter`     | `RatingAdapter`                                        | No                   | Noop              | Custom rating adapter (default is no-op)                                                     |\n| `linkOpener`        | `{ open: (url) => Promise<void> }`                     | No                   | `Linking.openURL` | Custom link/deep link opener                                                                 |\n| `analytics`         | `{ screen?: (node) => void, end?: (payload) => void }` | No                   | -                 | Analytics callbacks for tracking                                                             |\n\n### Example with All Props\n\n```tsx\nimport { Onboarding } from \"@codyzk/onboarding\n\";\nimport flow from \"./flows/onboarding.json\";\n\nfunction App() {\n  return (\n    <Onboarding\n      flow={flow}\n      theme=\"auto\"\n      onEnd={(result) => {\n        console.log(\"Onboarding ended:\", result);\n        if (result.dest === \"paywall\") {\n          navigation.navigate(\"Paywall\", result.payload);\n        } else {\n          navigation.navigate(\"Home\");\n        }\n      }}\n      permissionAdapter={customPermissionAdapter}\n      ratingAdapter={customRatingAdapter}\n      linkOpener={{\n        open: async (url) => {\n          // Custom deep link handling\n          await Linking.openURL(url);\n        },\n      }}\n      analytics={{\n        screen: (node) => {\n          analytics.track(\"Screen Viewed\", {\n            screenId: node.id,\n            screenType: node.type,\n          });\n        },\n        end: (payload) => {\n          analytics.track(\"Onboarding Completed\", payload);\n        },\n      }}\n    />\n  );\n}\n```\n\n## Theming\n\nThe SDK supports light, dark, and auto theming that matches your host app.\n\n### Theme Modes\n\n```tsx\n// Auto mode - follows system appearance (default)\n<Onboarding theme=\"auto\" ... />\n\n// Force light mode\n<Onboarding theme=\"light\" ... />\n\n// Force dark mode\n<Onboarding theme=\"dark\" ... />\n```\n\n### Per-Flow Theme Override\n\nSet a default theme in your flow JSON:\n\n```json\n{\n  \"id\": \"onboarding_v1\",\n  \"version\": 1,\n  \"start\": \"welcome\",\n  \"theme\": \"dark\",\n  \"nodes\": { ... }\n}\n```\n\n**Priority**: Component prop > Flow JSON > \"auto\" (system default)\n\n### Using Theme in Custom Components\n\n```tsx\nimport { useTheme } from \"@codyzk/onboarding\n\";\n\nfunction CustomComponent() {\n  const theme = useTheme();\n\n  return (\n    <View style={{ backgroundColor: theme.color.surface }}>\n      <Text style={{ color: theme.color.text }}>Custom Content</Text>\n    </View>\n  );\n}\n```\n\n## Permissions\n\nThe SDK includes a pluggable permission adapter system that works without native dependencies for MVP testing.\n\n### Default Behavior (Stub Adapter)\n\nBy default, the SDK uses an in-memory stub that returns deterministic results:\n\n```ts\nimport {\n  StubPermissionAdapter,\n  setPermissionAdapter,\n} from \"@codyzk/onboarding\n\";\n\n// Configure stub with default outcomes\nconst adapter = new StubPermissionAdapter({\n  camera: \"denied\",\n  location: \"granted\",\n  photoLibrary: \"denied\",\n});\n\nsetPermissionAdapter(adapter);\n\n// Later in dev/testing, change outcomes without rebuild\nadapter.set(\"camera\", \"granted\");\n```\n\n### Permission Types\n\nSupported permission types:\n\n- `camera`\n- `microphone`\n- `location`\n- `photoLibrary`\n- `contacts`\n- `calendar`\n- `reminders`\n- `speech`\n- `motion`\n- `bluetooth`\n- `notification`\n\n### Permission Status\n\nEach permission can have one of three states:\n\n- `granted` - User has granted permission\n- `denied` - User has not granted or explicitly denied\n- `blocked` - User has permanently denied (requires system settings)\n\n### Using in Flows\n\nDefine permission nodes in your flow JSON with optional customization:\n\n```json\n{\n  \"p1\": {\n    \"type\": \"permission\",\n    \"id\": \"p1\",\n    \"permission\": \"camera\",\n    \"title\": \"Camera Access Required\",\n    \"rationale\": \"We use your camera to scan documents and create quick posts.\",\n    \"allowLabel\": \"Enable Camera\",\n    \"notNowLabel\": \"Maybe Later\",\n    \"onGranted\": \"next_screen\",\n    \"onDenied\": \"skip_camera_features\"\n  }\n}\n```\n\n**Optional Fields:**\n\n- `title` - Screen title (default: \"Permission required\")\n- `rationale` - Explanation text (default: \"We need access to continue.\")\n- `allowLabel` - Primary button text (default: \"Allow\")\n- `notNowLabel` - Secondary button text (default: \"Not now\")\n\n### Custom Adapter (Production)\n\nFor production, implement the `PermissionAdapter` interface:\n\n```ts\nimport { PermissionAdapter, setPermissionAdapter } from \"@codyzk/onboarding\n\";\nimport {\n  check,\n  request,\n  openSettings,\n  PERMISSIONS,\n} from \"react-native-permissions\";\n\nclass NativePermissionAdapter implements PermissionAdapter {\n  async check(type: PermissionType): Promise<PermissionStatus> {\n    const result = await check(PERMISSIONS.IOS[type.toUpperCase()]);\n    return this.mapStatus(result);\n  }\n\n  async request(type: PermissionType): Promise<PermissionStatus> {\n    const result = await request(PERMISSIONS.IOS[type.toUpperCase()]);\n    return this.mapStatus(result);\n  }\n\n  async openSettings(): Promise<void> {\n    await openSettings();\n  }\n\n  private mapStatus(status: string): PermissionStatus {\n    if (status === \"granted\") return \"granted\";\n    if (status === \"blocked\") return \"blocked\";\n    return \"denied\";\n  }\n}\n\n// Swap in production adapter at app startup\nsetPermissionAdapter(new NativePermissionAdapter());\n```\n\n### Development Panel\n\nFor testing different permission flows, create a dev panel:\n\n```tsx\nimport {\n  StubPermissionAdapter,\n  setPermissionAdapter,\n} from \"@codyzk/onboarding\n\";\n\n// Initialize stub\nconst adapter = new StubPermissionAdapter();\nsetPermissionAdapter(adapter);\n\n// Dev UI to toggle outcomes\nfunction PermissionDevPanel() {\n  return (\n    <View>\n      <Button onPress={() => adapter.set(\"camera\", \"granted\")}>\n        Grant Camera\n      </Button>\n      <Button onPress={() => adapter.set(\"camera\", \"denied\")}>\n        Deny Camera\n      </Button>\n      <Button onPress={() => adapter.set(\"location\", \"granted\")}>\n        Grant Location\n      </Button>\n    </View>\n  );\n}\n```\n\n### Edge Cases\n\n- **Unknown permission type**: Treated as `denied` (won't crash)\n- **Missing `onGranted` or `onDenied`**: Flow ends to prevent getting stuck\n- **Adapter throws**: Treated as `denied` to avoid blocking user\n- **`blocked` status**: Routes to `onDenied` for MVP (later can show settings prompt)\n\n## Rating / Review Requests\n\nThe SDK includes a pluggable rating adapter for in-app review prompts.\n\n### Default Behavior (No-op Adapter)\n\nBy default, the SDK uses a no-op adapter that resolves immediately without showing any UI:\n\n```ts\nimport { NoopRatingAdapter, setRatingAdapter } from \"@codyzk/onboarding\n\";\n\n// Default behavior - no-op for MVP\n// Nothing happens when review is requested\n```\n\n### Using in Flows\n\nDefine rate nodes with two trigger modes:\n\n**Immediate Mode** (auto-trigger on node):\n\n```json\n{\n  \"rate1\": {\n    \"type\": \"rate\",\n    \"id\": \"rate1\",\n    \"trigger\": \"immediate\",\n    \"next\": \"next_screen\"\n  }\n}\n```\n\n**After-Next Mode** (show button):\n\n```json\n{\n  \"rate2\": {\n    \"type\": \"rate\",\n    \"id\": \"rate2\",\n    \"trigger\": \"after_next\",\n    \"next\": \"next_screen\"\n  }\n}\n```\n\n### Custom Adapter (Production)\n\nFor production, implement the `RatingAdapter` interface:\n\n```ts\nimport { RatingAdapter, setRatingAdapter } from \"@codyzk/onboarding\n\";\nimport { requestReview } from \"react-native-store-review\";\n\nclass NativeRatingAdapter implements RatingAdapter {\n  async requestReview(): Promise<void> {\n    try {\n      // iOS: StoreReviewController.requestReview()\n      // Android: Play In-App Review API\n      await requestReview();\n    } catch (error) {\n      // Swallow errors - never block flow\n      console.error(\"Review request failed:\", error);\n    }\n  }\n}\n\n// Set at app startup\nsetRatingAdapter(new NativeRatingAdapter());\n```\n\n### Timeout Protection\n\nThe rate node automatically times out after 2.5 seconds to prevent blocking:\n\n- If the adapter takes too long, flow continues anyway\n- User is never stuck waiting\n- Ensures smooth progression\n\n### Edge Cases\n\n- **No adapter provided**: Falls back to NoopRatingAdapter (no-op)\n- **Adapter throws**: Error caught, flow continues to next\n- **Timeout**: After 2.5s, flow continues regardless\n- **Multiple calls**: Safe to call multiple times (idempotency handled by adapter)\n\n## Flow JSON Schema\n\n### Screen Nodes (Component System v1)\n\nScreen nodes use the new component array system with tokens and flexible layouts:\n\n```typescript\n{\n  type: \"screen\",\n  id: string,\n  schemaVersion: 1,\n  tokens: {\n    colors: Record<string, string>,      // e.g., { \"brand\": \"#4F46E5\" }\n    spacing: Record<string, number>,     // e.g., { \"md\": 16 }\n    radii: Record<string, number>,       // e.g., { \"md\": 12 }\n    typography: Record<string, {         // e.g., { \"title\": { size: 28, weight: 700 } }\n      size: number,\n      lineHeight?: number,\n      weight?: number,\n      letterSpacing?: number\n    }>\n  },\n  components: Component[],               // Array of UI components\n  next?: string,                         // Optional next node reference\n  tags?: string[]                        // Optional tags for analytics\n}\n```\n\n### Component Types\n\n**Box (Container)**\n\n```json\n{\n  \"type\": \"box\",\n  \"id\": \"unique-id\",\n  \"style\": {\n    \"flex\": 1,\n    \"flexDirection\": \"column\",\n    \"gap\": \"md\",\n    \"padding\": \"lg\",\n    \"justifyContent\": \"center\",\n    \"alignItems\": \"center\"\n  },\n  \"children\": [\n    /* nested components */\n  ]\n}\n```\n\n**Text**\n\n```json\n{\n  \"type\": \"text\",\n  \"id\": \"unique-id\",\n  \"content\": \"Your text here\",\n  \"typography\": {\n    \"preset\": \"title\", // or direct values\n    \"size\": 24,\n    \"weight\": 600\n  },\n  \"style\": {\n    \"color\": \"text\",\n    \"textAlign\": \"center\"\n  }\n}\n```\n\n**Button**\n\n```json\n{\n  \"type\": \"button\",\n  \"id\": \"unique-id\",\n  \"label\": \"Button Text\",\n  \"action\": {\n    \"kind\": \"next\",\n    \"to\": \"next-node-id\"\n  },\n  \"style\": {\n    \"backgroundColor\": \"brand\",\n    \"padding\": \"md\",\n    \"borderRadius\": \"md\",\n    \"color\": \"#FFFFFF\"\n  },\n  \"a11yLabel\": \"Accessibility label\"\n}\n```\n\n**Image**\n\n```json\n{\n  \"type\": \"image\",\n  \"id\": \"unique-id\",\n  \"url\": \"https://example.com/image.png\",\n  \"aspectRatio\": 1.6,\n  \"resizeMode\": \"cover\",\n  \"style\": {\n    \"borderRadius\": \"md\"\n  }\n}\n```\n\n**Spacer**\n\n```json\n{\n  \"type\": \"spacer\",\n  \"id\": \"unique-id\",\n  \"size\": \"lg\" // or numeric: 24\n}\n```\n\n**Divider**\n\n```json\n{\n  \"type\": \"divider\",\n  \"id\": \"unique-id\",\n  \"style\": {\n    \"backgroundColor\": \"surface\",\n    \"height\": 1\n  }\n}\n```\n\n### Other Node Types\n\n**Permission Node**\n\n```json\n{\n  \"type\": \"permission\",\n  \"id\": \"unique-id\",\n  \"permission\": \"camera\",\n  \"title\": \"Camera Access\",\n  \"rationale\": \"We need camera access to scan documents\",\n  \"allowLabel\": \"Enable Camera\",\n  \"notNowLabel\": \"Maybe Later\",\n  \"onGranted\": \"next-node-id\",\n  \"onDenied\": \"skip-node-id\"\n}\n```\n\n**Rate Node**\n\n```json\n{\n  \"type\": \"rate\",\n  \"id\": \"unique-id\",\n  \"trigger\": \"immediate\", // or \"after_next\"\n  \"next\": \"next-node-id\"\n}\n```\n\n**End Node**\n\n```json\n{\n  \"type\": \"end\",\n  \"id\": \"unique-id\",\n  \"result\": \"home\", // or \"paywall\" | \"custom\"\n  \"payload\": {\n    \"completed\": true,\n    \"any\": \"custom data\"\n  }\n}\n```\n\n## Examples\n\nSee example flows in the [`flows/`](./flows/) directory:\n\n- [`component-system.json`](./flows/component-system.json) - Full featured example with tokens and layouts\n- [`README.md`](./flows/README.md) - Migration guide and detailed documentation\n\n## Documentation\n\n- 📖 [**Implementation Guide**](./IMPLEMENTATION.md) - Complete guide to building flows\n- 🎨 [**Token System**](./IMPLEMENTATION.md#token-system) - Design tokens and theming\n- 🧩 [**Component Reference**](./IMPLEMENTATION.md#component-types) - All available components\n- 🔌 [**Adapters**](./IMPLEMENTATION.md#adapters) - Custom permissions and ratings\n\n## License\n\nMIT\n","readmeFilename":"README.md"}