# @askable-ui/react-native

React Native bindings for askable-ui.

This initial slice focuses on explicit mobile interactions: `useAskable()` provides a context backed by `@askable-ui/core`, `useAskableScreen()` lets you mirror screen focus into that context, `useAskableScrollView()` mirrors raw `ScrollView` measurement into that context, `useAskableVisibility()` mirrors viewability updates from mobile lists, and `<Askable />` turns `onPress` / `onLongPress` interactions into prompt-ready focus updates.

A runnable Expo reference app lives in [`examples/react-native-expo`](https://github.com/askable-ui/askable/tree/main/examples/react-native-expo).

## Install

```bash
npm install @askable-ui/react-native @askable-ui/core
```

---

## `<Askable>`

Clones a single React Native pressable child and merges focus updates into its `onPress` and `onLongPress` handlers. Nested `<Askable>` wrappers also contribute ancestor segments, so press-driven mobile flows can serialize the same hierarchy paths as DOM-based web flows.

```tsx
import { Pressable, Text } from 'react-native';
import { Askable, useAskable } from '@askable-ui/react-native';

function RevenueCard() {
  const { ctx } = useAskable();

  return (
    <Askable ctx={ctx} meta={{ widget: 'revenue' }} text="Revenue card">
      <Pressable>
        <Text>Revenue</Text>
      </Pressable>
    </Askable>
  );
}
```

**Props:**

| Prop | Type | Description |
|---|---|---|
| `ctx` | `AskableContext` | Context instance that receives focus updates |
| `meta` | `Record<string, unknown> \| string` | Metadata pushed into the context on press |
| `scope` | `string` | Optional category stored with press-driven focus for scoped prompt/history queries |
| `text` | `string` | Optional human-readable label stored alongside `meta` |
| `children` | `ReactElement` | A single child that accepts `onPress` / `onLongPress` props |

```tsx
<Askable ctx={ctx} meta={{ view: 'dashboard' }} text="Dashboard" scope="analytics">
  <Askable ctx={ctx} meta={{ tab: 'finance' }} text="Finance" scope="analytics">
    <Askable ctx={ctx} meta={{ metric: 'revenue' }} text="Revenue card" scope="analytics">
      <Pressable>
        <Text>Revenue</Text>
      </Pressable>
    </Askable>
  </Askable>
</Askable>

ctx.toPromptContext();
// → "User is focused on: — view: dashboard > tab: finance > metric: revenue — value \"Revenue card\""
```

---

## `useAskable(options?)`

Returns a React Native-friendly wrapper around an `AskableContext`.

```ts
import { useAskable } from '@askable-ui/react-native';

const { focus, promptContext, ctx } = useAskable();
```

**Options:**

| Option | Type | Description |
|---|---|---|
| `ctx` | `AskableContext` | Reuse an existing context instead of creating a new one |
| `name` | `string` | Optional context name passed through to `createAskableContext()` |
| `viewport` | `boolean` | Optional viewport-aware core context mode |
| `events` | `AskableEvent[]` | Forwarded to the underlying core context |

**Returns:**

| Value | Type | Description |
|---|---|---|
| `focus` | `AskableFocus \| null` | Current focus created by press-driven updates |
| `promptContext` | `string` | Natural-language context string for your LLM prompt |
| `ctx` | `AskableContext` | Full context instance for `push()`, `clear()`, `toHistoryContext()`, etc. |

### Inspector

React Native does **not** use the floating browser overlay inspector. `createAskableInspector()` is DOM-based, so it is only relevant on web targets.

For React Native development, the equivalent debugging pattern is to inspect `focus`, `promptContext`, and `ctx.toHistoryContext()` directly in your own debug UI or logs:

```tsx
function AskableDebugPanel() {
  const { focus, promptContext } = useAskable();

  return __DEV__ ? (
    <View>
      <Text>{focus ? JSON.stringify(focus.meta) : 'No focus yet'}</Text>
      <Text>{promptContext}</Text>
    </View>
  ) : null;
}
```

If you share a custom `ctx` across a screen, read from that same `ctx` in your debug panel so the mobile debugging surface reflects the same Askable context as your press/visibility helpers.

### Shared vs private/custom contexts

React Native differs from the web adapters: `useAskable()` creates a **private** context per hook call unless you provide `ctx`.

- **Private default** — each `useAskable()` call gets its own `AskableContext`.
- **Custom shared context** — create one context and pass it to `useAskable({ ctx })`, `<Askable ctx={ctx} />`, `useAskableScreen({ ctx })`, `useAskableScrollView({ ctx })`, and `useAskableVisibility({ ctx })` when one screen or app surface should share focus/history.
- **Multiple isolated surfaces** — create separate contexts when different tabs/screens/chats should not affect each other.

```tsx
import { createAskableContext } from '@askable-ui/core';
import { Askable, useAskable, useAskableScreen } from '@askable-ui/react-native';

// Shared screen-level context
const screenCtx = createAskableContext();

function RevenueScreen() {
  const askable = useAskable({ ctx: screenCtx });

  useAskableScreen({
    ctx: screenCtx,
    meta: { screen: 'RevenueScreen' },
    text: 'Revenue screen',
  });

  return (
    <Askable ctx={screenCtx} meta={{ widget: 'revenue' }} text="Revenue card">
      <Pressable>{/* ... */}</Pressable>
    </Askable>
  );
}

// Separate private context for another surface
function SupportChatScreen() {
  const support = useAskable();
  return null;
}
```

Use the private default for isolated screens. Provide `ctx` explicitly when multiple React Native helpers/components should cooperate on one Askable context.

## `useAskableScreen(options)`

Pushes screen-level context into the shared `AskableContext` while a screen is active.

```tsx
import { useIsFocused } from '@react-navigation/native';
import { useAskable, useAskableScreen } from '@askable-ui/react-native';

function RevenueScreen() {
  const isFocused = useIsFocused();
  const { ctx } = useAskable();

  useAskableScreen({
    ctx,
    active: isFocused,
    meta: { screen: 'RevenueScreen' },
    text: 'Revenue screen',
  });

  return null;
}
```

**Options:**

| Option | Type | Description |
|---|---|---|
| `ctx` | `AskableContext` | Reuse an existing context instead of creating a new one |
| `meta` | `Record<string, unknown> \| string` | Screen metadata pushed when the screen is active |
| `text` | `string` | Optional label stored alongside the screen metadata |
| `active` | `boolean` | Whether the screen is currently focused. Default: `true` |
| `clearOnBlur` | `boolean` | Clear the context when the screen becomes inactive. Default: `true` |
| `name` / `viewport` / `events` | Core options | Forwarded when the hook creates its own context |

## `useAskableScrollView(options)`

Mirrors raw `ScrollView` measurement into the shared `AskableContext` by tracking child layouts and selecting the top visible measured item.

```tsx
import { Pressable, ScrollView, Text } from 'react-native';
import { Askable, useAskable, useAskableScrollView } from '@askable-ui/react-native';

const cards = [
  { id: 'rev', title: 'Revenue', meta: { widget: 'revenue' } },
  { id: 'pipe', title: 'Pipeline', meta: { widget: 'pipeline' } },
];

function DashboardFeed() {
  const { ctx } = useAskable();
  const { onScroll, createOnItemLayout } = useAskableScrollView({
    ctx,
    getMeta: (card) => ({ ...card.meta, visible: true }),
    getText: (card) => `${card.title} is leading the dashboard scroll view`,
  });

  return (
    <ScrollView onScroll={onScroll} scrollEventThrottle={16}>
      {cards.map((card) => (
        <Askable key={card.id} ctx={ctx} meta={card.meta} text={card.title}>
          <Pressable onLayout={createOnItemLayout(card.id, card)}>
            <Text>{card.title}</Text>
          </Pressable>
        </Askable>
      ))}
    </ScrollView>
  );
}
```

**Options:**

| Option | Type | Description |
|---|---|---|
| `ctx` | `AskableContext` | Reuse an existing context instead of creating a new one |
| `active` | `boolean` | Whether scroll updates should currently affect focus. Default: `true` |
| `clearOnBlur` | `boolean` | Clear existing scroll focus when tracking becomes inactive. Default: `true` |
| `getMeta` | `(item, measured) => Record<string, unknown> \| string` | Maps the visible measured item into askable metadata |
| `getText` | `(item, measured) => string` | Optional human-readable label stored alongside the metadata |
| `selectVisible` | `(items) => item \| null` | Override which measured item should win focus. Default: the top-most visible item |
| `name` / `viewport` / `events` | Core options | Forwarded when the hook creates its own context |

**Returns:**

| Value | Type | Description |
|---|---|---|
| `ctx` | `AskableContext` | Context instance used for scroll-driven focus updates |
| `onScroll` | `(event) => void` | Attach to `ScrollView.onScroll` |
| `measureItem` | `(key, item, layout) => void` | Manually register/update a child layout |
| `unmeasureItem` | `(key) => void` | Remove a child layout when it leaves the tree |
| `clearVisibleItem` | `() => void` | Clear the current scroll-driven focus |
| `createOnItemLayout` | `(key, item) => onLayoutHandler` | Convenience helper for `onLayout` wiring |

## `useAskableVisibility(options)`

Mirrors `FlatList` / `SectionList` viewability callbacks into the shared `AskableContext`.

```tsx
import { FlatList, Text, View } from 'react-native';
import { useAskable, useAskableVisibility } from '@askable-ui/react-native';

const rows = [
  { id: 'deal-1', title: 'Enterprise renewal' },
  { id: 'deal-2', title: 'Expansion pipeline' },
];

function DealsList() {
  const { ctx } = useAskable();
  const { onViewableItemsChanged } = useAskableVisibility({
    ctx,
    getMeta: (item) => ({ dealId: item.id }),
    getText: (item) => item.title,
  });

  return (
    <FlatList
      data={rows}
      keyExtractor={(item) => item.id}
      onViewableItemsChanged={onViewableItemsChanged}
      renderItem={({ item }) => (
        <View>
          <Text>{item.title}</Text>
        </View>
      )}
    />
  );
}
```

**Options:**

| Option | Type | Description |
|---|---|---|
| `ctx` | `AskableContext` | Reuse an existing context instead of creating a new one |
| `active` | `boolean` | Whether visibility updates should currently affect focus. Default: `true` |
| `clearOnBlur` | `boolean` | Clear existing visibility focus when tracking becomes inactive. Default: `true` |
| `getMeta` | `(item, token) => Record<string, unknown> \| string` | Maps the visible row into askable metadata |
| `getText` | `(item, token) => string` | Optional human-readable label stored alongside the metadata |
| `selectViewable` | `(tokens) => token \| null` | Override which visible token should win focus. Default: the first visible item |
| `name` / `viewport` / `events` | Core options | Forwarded when the hook creates its own context |

## Notes

- This adapter currently covers press-driven interactions, lightweight screen-awareness, raw `ScrollView` measurement, and list viewability callbacks.
- `useAskableScreen()` is designed to pair with React Navigation's `useIsFocused()` or a similar focus signal.
- `useAskableScrollView()` is a good fit for custom dashboard layouts built with `ScrollView`.
- `useAskableVisibility()` is ideal for `FlatList` / `SectionList`.
- Existing child `onPress` / `onLongPress` handlers are preserved and still run.
