// src/core/vision-fields.ts
var INTERACTIVE_HANDLER_NAMES = /* @__PURE__ */ new Set([
  "onPress",
  "onPressIn",
  "onPressOut",
  "onLongPress",
  "onChangeText",
  "onValueChange",
  "onChange",
  "onSubmitEditing",
  "onClick",
  "onTouchStart",
  "onTouchEnd"
]);
var INTERACTIVE_TYPES = /* @__PURE__ */ new Set([
  "button",
  "input",
  "switch",
  "checkbox",
  "radio",
  "touchable",
  "pressable",
  "listItem"
]);
function roleForType(type) {
  switch (type) {
    case "button":
    case "touchable":
    case "pressable":
      return "button";
    case "input":
      return "textbox";
    case "text":
      return "text";
    case "switch":
      return "switch";
    case "checkbox":
      return "checkbox";
    case "radio":
      return "radio";
    case "image":
      return "img";
    case "list":
      return "list";
    case "listItem":
      return "listitem";
    case "modal":
      return "dialog";
    case "scroll":
    case "view":
    case "custom":
    default:
      return void 0;
  }
}
function deriveInteractable(type, handlerNames) {
  if (INTERACTIVE_TYPES.has(type)) return true;
  if (handlerNames) {
    for (const name of handlerNames) {
      if (INTERACTIVE_HANDLER_NAMES.has(name)) return true;
    }
  }
  return false;
}
function deriveText(state, label) {
  const candidates = [state.value, state.textContent, label];
  for (const c of candidates) {
    if (typeof c === "string") {
      const trimmed = c.trim();
      if (trimmed.length > 0) return trimmed;
    }
  }
  return void 0;
}
var NAMED_COLORS = {
  black: { r: 0, g: 0, b: 0 },
  white: { r: 255, g: 255, b: 255 },
  red: { r: 255, g: 0, b: 0 },
  green: { r: 0, g: 128, b: 0 },
  blue: { r: 0, g: 0, b: 255 },
  gray: { r: 128, g: 128, b: 128 },
  grey: { r: 128, g: 128, b: 128 },
  silver: { r: 192, g: 192, b: 192 },
  transparent: { r: 0, g: 0, b: 0 },
  orange: { r: 255, g: 165, b: 0 },
  yellow: { r: 255, g: 255, b: 0 },
  purple: { r: 128, g: 0, b: 128 },
  navy: { r: 0, g: 0, b: 128 }
};
var clamp255 = (n) => Math.max(0, Math.min(255, Math.round(n)));
function parseColorToRgb(input) {
  if (typeof input !== "string") return void 0;
  const raw = input.trim().toLowerCase();
  if (raw.length === 0) return void 0;
  if (Object.prototype.hasOwnProperty.call(NAMED_COLORS, raw)) {
    return NAMED_COLORS[raw];
  }
  if (raw.startsWith("#")) {
    const hex = raw.slice(1);
    if (hex.length === 3 || hex.length === 4) {
      const r = parseInt(hex[0] + hex[0], 16);
      const g = parseInt(hex[1] + hex[1], 16);
      const b = parseInt(hex[2] + hex[2], 16);
      if ([r, g, b].every((v) => Number.isFinite(v))) return { r, g, b };
      return void 0;
    }
    if (hex.length === 6 || hex.length === 8) {
      const r = parseInt(hex.slice(0, 2), 16);
      const g = parseInt(hex.slice(2, 4), 16);
      const b = parseInt(hex.slice(4, 6), 16);
      if ([r, g, b].every((v) => Number.isFinite(v))) return { r, g, b };
      return void 0;
    }
    return void 0;
  }
  const rgbMatch = raw.match(/^rgba?\(([^)]+)\)$/);
  if (rgbMatch) {
    const parts = rgbMatch[1].split(",").map((p) => p.trim());
    if (parts.length < 3) return void 0;
    const channel = (p) => {
      if (p.endsWith("%")) {
        const pct = parseFloat(p.slice(0, -1));
        if (!Number.isFinite(pct)) return void 0;
        return clamp255(pct / 100 * 255);
      }
      const n = parseFloat(p);
      if (!Number.isFinite(n)) return void 0;
      return clamp255(n);
    };
    const r = channel(parts[0]);
    const g = channel(parts[1]);
    const b = channel(parts[2]);
    if (r === void 0 || g === void 0 || b === void 0) return void 0;
    return { r, g, b };
  }
  return void 0;
}
function parseFontSizePx(input) {
  if (typeof input === "number") {
    return Number.isFinite(input) && input > 0 ? input : void 0;
  }
  if (typeof input === "string") {
    const m = input.trim().match(/^(-?\d*\.?\d+)/);
    if (m) {
      const n = parseFloat(m[1]);
      return Number.isFinite(n) && n > 0 ? n : void 0;
    }
  }
  return void 0;
}
function projectVisionFields(args) {
  const { type, label, handlerNames, state, flatStyle } = args;
  const fields = {
    interactable: deriveInteractable(type, handlerNames)
  };
  const text = deriveText(state, label);
  if (text !== void 0) fields.text = text;
  const role = roleForType(type);
  if (role !== void 0) fields.role = role;
  if (flatStyle) {
    const fg = parseColorToRgb(flatStyle.color);
    if (fg !== void 0) fields.fg_color = fg;
    const bg = parseColorToRgb(flatStyle.backgroundColor);
    if (bg !== void 0) fields.bg_color = bg;
    const size = parseFontSizePx(flatStyle.fontSize);
    if (size !== void 0) fields.font_size_px = size;
    if (typeof flatStyle.fontFamily === "string" && flatStyle.fontFamily.trim().length > 0) {
      fields.font_family = flatStyle.fontFamily;
    }
  }
  return fields;
}

// src/core/registry.ts
function extractHandlerNames(props) {
  if (!props) return [];
  return Object.keys(props).filter((k) => typeof props[k] === "function");
}
function matchesCurrentRoute(registrationRoute, currentRoute) {
  if (registrationRoute == null || registrationRoute === "") return true;
  return registrationRoute === currentRoute;
}
function projectBbox(state, visibility, pixelRatio = 1) {
  if (visibility !== "visible") return void 0;
  const layout = state.layout;
  if (!layout) return void 0;
  const ratio = pixelRatio > 0 && Number.isFinite(pixelRatio) ? pixelRatio : 1;
  const toPx = (v) => Math.max(0, Math.round(v * ratio));
  const hasPage = typeof layout.pageX === "number" && typeof layout.pageY === "number" && Number.isFinite(layout.pageX) && Number.isFinite(layout.pageY);
  const originX = hasPage ? layout.pageX : layout.x;
  const originY = hasPage ? layout.pageY : layout.y;
  if (typeof originX !== "number" || typeof originY !== "number" || typeof layout.width !== "number" || typeof layout.height !== "number" || !Number.isFinite(originX) || !Number.isFinite(originY) || !Number.isFinite(layout.width) || !Number.isFinite(layout.height)) {
    return void 0;
  }
  return {
    x: toPx(originX),
    y: toPx(originY),
    w: toPx(layout.width),
    h: toPx(layout.height)
  };
}
function inferActions(type) {
  const baseActions = ["focus", "blur"];
  switch (type) {
    case "button":
    case "touchable":
    case "pressable":
      return [...baseActions, "click", "press", "longPress", "doubleTap"];
    case "input":
      return [...baseActions, "click", "press", "type", "setValue", "clear"];
    case "text":
      return [...baseActions, "click", "press", "longPress"];
    case "view":
      return [...baseActions, "click", "press"];
    case "scroll":
      return [...baseActions, "scroll", "swipe"];
    case "list":
      return [...baseActions, "scroll", "swipe"];
    case "listItem":
      return [...baseActions, "click", "press", "longPress", "swipe"];
    case "switch":
    case "checkbox":
      return [...baseActions, "click", "press", "toggle"];
    case "radio":
      return [...baseActions, "click", "press"];
    case "image":
      return [...baseActions, "click", "press", "longPress"];
    case "modal":
      return ["focus", "blur"];
    case "custom":
    default:
      return [...baseActions, "click", "press"];
  }
}
var NativeUIBridgeRegistry = class {
  constructor(config = {}) {
    this.elements = /* @__PURE__ */ new Map();
    this.components = /* @__PURE__ */ new Map();
    this.workflows = /* @__PURE__ */ new Map();
    this.eventListeners = /* @__PURE__ */ new Map();
    this.enrichers = {};
    this.snapshotExtras = /* @__PURE__ */ new Map();
    /**
     * App identification metadata (appId/appName/appType/framework). Set via
     * `setAppInfo` — typically wired in the server constructor from the
     * `NativeUIBridgeConfig` the host passes to `UIBridgeNativeProvider`.
     *
     * When set, `createSnapshot` includes it on the resulting snapshot so
     * agents can identify the app from snapshot bytes alone (parity with the
     * `uiBridge` block on the `health` response). Snapshots created before
     * this is wired simply omit the field.
     */
    this.appInfo = null;
    /**
     * Optional route provider. When set, `createSnapshot` reads
     * `currentRoute`/`segments` from it whenever the caller does not pass an
     * explicit `routeInfo` argument. This lets the default `getSnapshot` HTTP
     * handler — which has no direct reference to the `NativeUIBridgeServer`
     * instance — still emit a populated `currentRoute` field.
     *
     * Wired by `NativeUIBridgeServer.setRouteProvider`, which forwards the
     * provider into the registry alongside the per-handler overrides it
     * already installs.
     */
    this.routeProvider = null;
    /**
     * Device pixel ratio used to project `state.layout` (logical dp) into the
     * physical-pixel `bbox` emitted on each snapshot element. Defaults to `1`
     * (dp ≈ px) so callers/tests that never inject a ratio still get a usable
     * box; the React provider wires the real `PixelRatio.get()` via
     * {@link setPixelRatio}. Read from a non-React core module on purpose — see
     * the `projectBbox` doc comment for why we don't import `react-native` here.
     */
    this.pixelRatio = 1;
    /**
     * Sticky flag: flips `true` the first time any element is registered and
     * stays `true` even after elements are unregistered. Lets agents distinguish
     * "this route never wired any elements" from "this route registered then
     * unmounted".
     */
    this.everHadRegistrations = false;
    this.config = config;
  }
  // ============================================================================
  // App Info & Route Provider
  // ============================================================================
  /**
   * Set the app identification metadata returned alongside snapshots.
   * Idempotent — re-calling with the same shape is a no-op.
   * Pass `null` (or omit a previous call) to clear.
   */
  setAppInfo(info) {
    this.appInfo = info ?? null;
  }
  /** Read-only accessor for the registered app info, if any. */
  getAppInfo() {
    return this.appInfo;
  }
  /**
   * Set a route provider whose `getCurrentRoute`/`getSegments` are read by
   * `createSnapshot` when no explicit `routeInfo` argument is supplied.
   *
   * Call with `null` to detach (e.g. when the host swaps providers on HMR).
   */
  setRouteProvider(provider) {
    this.routeProvider = provider ?? null;
  }
  /** Read-only accessor for the registered route provider, if any. */
  getRouteProvider() {
    return this.routeProvider;
  }
  /**
   * Set the device pixel ratio used to project per-element `bbox` geometry
   * into physical pixels. The React provider passes `PixelRatio.get()` here
   * once at startup. Ignores non-finite / non-positive values (keeps the
   * previous ratio) so a bad call can't zero out every bbox.
   */
  setPixelRatio(ratio) {
    if (typeof ratio === "number" && Number.isFinite(ratio) && ratio > 0) {
      this.pixelRatio = ratio;
    }
  }
  /** Read-only accessor for the device pixel ratio used for bbox projection. */
  getPixelRatio() {
    return this.pixelRatio;
  }
  // ============================================================================
  // Snapshot Enricher Slots
  // ============================================================================
  /**
   * Register/replace canonical enrichers (modal/toast/undo). HMR-safe — calling
   * with a partial set merges into existing slots instead of clobbering them.
   */
  setEnrichers(e) {
    this.enrichers = { ...this.enrichers, ...e };
  }
  /**
   * Read-only accessor for the canonical enrichers slot. Returned object is
   * the live reference, so callers can detect "no detector wired" via
   * `getEnrichers().modalDetector === undefined`. Used by the test-hook HTTP
   * handlers (`control/modal/push`, `control/modal/dismiss/:id`) which need
   * to drive the ModalDetector directly without re-instantiating it.
   */
  getEnrichers() {
    return this.enrichers;
  }
  /**
   * Register a custom snapshot enricher. The returned object will be
   * `Object.assign`ed onto the snapshot, so use unique top-level keys to avoid
   * clobbering canonical fields. Returns a disposer.
   */
  registerSnapshotEnricher(name, fn) {
    this.snapshotExtras.set(name, fn);
    return () => this.unregisterSnapshotEnricher(name);
  }
  /** Remove a custom snapshot enricher by name */
  unregisterSnapshotEnricher(name) {
    this.snapshotExtras.delete(name);
  }
  // ============================================================================
  // Element Management
  // ============================================================================
  /**
   * Register a native element
   */
  registerElement(id, ref, options = {}) {
    const {
      type = "custom",
      label,
      actions = inferActions(type),
      customActions,
      props,
      treePath = id,
      testId,
      accessibilityLabel,
      registrationRoute,
      flatStyle,
      stateStyles
    } = options;
    const getState = () => {
      const element = ref.current;
      if (!element) {
        return {
          mounted: false,
          visible: false,
          enabled: false,
          focused: false,
          layout: null
        };
      }
      const stored = this.elements.get(id);
      if (stored && stored.getState !== getState) {
        return stored.getState();
      }
      return {
        mounted: true,
        visible: true,
        enabled: true,
        focused: false,
        layout: null
      };
    };
    const getIdentifier = () => ({
      uiId: id,
      testId: testId || id,
      accessibilityLabel,
      treePath
    });
    const registered = {
      id,
      ref,
      type,
      label,
      actions,
      customActions,
      props,
      getState,
      getIdentifier,
      registeredAt: Date.now(),
      mounted: true,
      registrationRoute: registrationRoute ?? null,
      flatStyle,
      stateStyles
    };
    this.elements.set(id, registered);
    this.everHadRegistrations = true;
    this.emit("element:registered", { id, type, label });
    if (this.config.verbose) {
      console.log(`[ui-bridge-native] Registered element: ${id} (${type})`);
    }
    const seededState = {
      mounted: true,
      visible: true,
      enabled: true,
      focused: false,
      layout: null
    };
    if (type === "input" && typeof props?.value === "string") {
      seededState.value = props.value;
    }
    if (type === "text" && typeof label === "string") {
      seededState.value = label;
    }
    this.updateElementState(id, seededState);
    return registered;
  }
  /**
   * Unregister an element
   */
  unregisterElement(id) {
    const element = this.elements.get(id);
    if (element) {
      this.elements.delete(id);
      this.emit("element:unregistered", { id });
      if (this.config.verbose) {
        console.log(`[ui-bridge-native] Unregistered element: ${id}`);
      }
    }
  }
  /**
   * Get a registered element
   */
  getElement(id) {
    return this.elements.get(id);
  }
  /**
   * Get all registered elements
   */
  getAllElements() {
    return Array.from(this.elements.values());
  }
  /**
   * Get all registered elements that are visible AND have a measured layout.
   *
   * This is the *strict* visibility filter: requires `state.visible === true`
   * AND `state.layout !== null`. Use this when you need both signals
   * (e.g. coord-based tap, where you need an actual rect to hit-test against).
   *
   * Callers driving an LLM agent typically want
   * {@link getMountedVisibleElements} instead — that variant accepts
   * elements that *should* be on screen (visible:true) but whose first
   * `onLayout` has not yet fired. Without it, a snapshot taken in the gap
   * between mount and the first layout pass returns 0 elements on a fully
   * populated screen.
   */
  getVisibleElements() {
    return this.getAllElements().filter((e) => {
      const state = e.getState();
      return state.visible && state.layout !== null;
    });
  }
  /**
   * Get all registered elements marked as visible (regardless of layout).
   *
   * Looser than {@link getVisibleElements}: includes elements whose
   * `onLayout` callback has not yet fired (so `layout === null`). React
   * Native's `onLayout` is async — there's a one-tick window after mount
   * where elements are in the tree, the user *sees* them, but the
   * registry hasn't received their measurements yet. Excluding those
   * elements makes `getSnapshot?visibleOnly=true` return 0 on a populated
   * screen, which is a worse failure mode than including a few elements
   * we can't yet locate spatially.
   *
   * Used by `createSnapshot`'s `visibleOnly` filter and by the
   * `getSnapshot` / `getElements` handlers. Each emitted element carries a
   * `visibility` discriminator (`"visible"` / `"likely-visible"`) so
   * agents that *do* require a known rect can branch downstream.
   */
  getMountedVisibleElements() {
    return this.getAllElements().filter((e) => e.getState().visible);
  }
  /**
   * Get elements registered on a specific route (for page-scoped filtering)
   */
  getElementsForRoute(route) {
    return this.getAllElements().filter((e) => e.registrationRoute === route);
  }
  /**
   * Mark elements registered on a route as off-screen (visible: false, layout: null).
   *
   * Use this when a screen loses focus but stays mounted — common in React Navigation
   * tab navigators where inactive tabs remain in the tree. Without this call, stale
   * `layout` data lingers in snapshots and makes off-screen elements look rendered.
   *
   * Does NOT unregister the elements — they stay registered so the user's next visit
   * re-measures them via `onLayout` without re-mount cost. Elements without a
   * `registrationRoute` (app-wide registrations) are untouched.
   */
  markRouteOffscreen(route) {
    if (route == null || route === "") {
      if (this.config.verbose) {
        console.warn(
          `[ui-bridge-native] markRouteOffscreen called with null/empty route \u2014 ignoring`
        );
      }
      return;
    }
    let cleared = 0;
    for (const element of this.elements.values()) {
      if (element.registrationRoute === route) {
        this.updateElementState(element.id, {
          visible: false,
          layout: null
        });
        cleared++;
      }
    }
    if (this.config.verbose && cleared > 0) {
      console.log(`[ui-bridge-native] Marked ${cleared} elements offscreen for route: ${route}`);
    }
  }
  /**
   * Re-measure every mounted-visible element's geometry via its stored React
   * ref and write the fresh coordinates into the registry ("measure-on-
   * snapshot").
   *
   * Why: element geometry is otherwise captured ONCE at mount time
   * (`onLayout` + `measureInWindow` in `useUIElement`) and RN never re-fires
   * `onLayout` on pure translation — scrolling a virtualized list
   * (FlashList), recycling a cell, a keyboard shift or an orientation change
   * all move elements WITHOUT updating the stored `state.layout`. Snapshot
   * consumers (vision analyzers hit-testing `bbox`, coord-based tap) then
   * operate on stale positions. Re-measuring at READ time is correct
   * regardless of what moved the element and needs zero per-consumer wiring:
   * every registry entry already stores its ref (see `registerElement`).
   *
   * Per-element outcomes:
   *   - non-zero dims → write a fresh full layout (same state shape as the
   *     mount-time measure loop in `useUIElement`); counted as `measured`.
   *   - zero dims AND the element previously had a non-null `state.layout` →
   *     the view collapsed / left the screen; clear to
   *     `{ visible: false, layout: null }` (same shape `markRouteOffscreen`
   *     writes); counted as `cleared`.
   *   - zero dims and it never had a layout → leave state untouched (element
   *     in the mount→first-onLayout gap; don't demote `likely-visible`);
   *     counted as `skipped`.
   *   - no ref / no callable `measureInWindow` (test fixtures, web) → leave
   *     untouched; counted as `skipped`.
   *
   * The whole sweep races an overall timeout (default 250 ms) so a dead ref
   * whose `measureInWindow` never calls back can never hang a snapshot: the
   * returned promise resolves at the deadline with whatever counts have
   * accumulated. Late callbacks may still write state afterwards — harmless,
   * freshest data wins. Never throws.
   *
   * Non-React core module on purpose — `measureInWindow` is duck-typed off
   * the stored ref rather than imported from `react-native` (see the
   * `projectBbox` doc comment for why this file must not import RN).
   */
  async refreshMeasurements(options) {
    const timeoutMs = typeof options?.timeoutMs === "number" && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 250;
    const counts = { measured: 0, cleared: 0, skipped: 0 };
    const pending = [];
    for (const element of this.getMountedVisibleElements()) {
      const node = element.ref?.current;
      if (!node || typeof node.measureInWindow !== "function") {
        counts.skipped++;
        continue;
      }
      const id = element.id;
      pending.push(
        new Promise((resolve) => {
          let settled = false;
          try {
            node.measureInWindow((pageX, pageY, w, h) => {
              if (settled) return;
              settled = true;
              try {
                if (w > 0 && h > 0) {
                  this.updateElementState(id, {
                    mounted: true,
                    visible: true,
                    enabled: true,
                    focused: false,
                    layout: { x: pageX, y: pageY, width: w, height: h, pageX, pageY }
                  });
                  counts.measured++;
                } else if (this.elements.get(id)?.getState().layout != null) {
                  this.updateElementState(id, { visible: false, layout: null });
                  counts.cleared++;
                } else {
                  counts.skipped++;
                }
              } finally {
                resolve();
              }
            });
          } catch {
            if (!settled) {
              settled = true;
              counts.skipped++;
              resolve();
            }
          }
        })
      );
    }
    if (pending.length > 0) {
      await new Promise((resolve) => {
        const timer = setTimeout(resolve, timeoutMs);
        void Promise.all(pending).then(() => {
          clearTimeout(timer);
          resolve();
        });
      });
    }
    return counts;
  }
  /**
   * Update element state
   */
  updateElementState(id, state) {
    const element = this.elements.get(id);
    if (element) {
      const currentState = element.getState();
      const newState = { ...currentState, ...state };
      const updated = {
        ...element,
        getState: () => newState
      };
      this.elements.set(id, updated);
      this.emit("element:stateChanged", { id, state: newState });
    }
  }
  /**
   * Update an element's descriptive metadata (label / accessibilityLabel /
   * testId). Sister to {@link updateElementState} (which carries layout +
   * visibility) and {@link updateElementProps} (which carries event handlers).
   *
   * Idempotent — if every passed field equals the existing value, the call
   * returns without mutating the entry or emitting an event. This keeps
   * `useUIElement().updateLabel(...)` cheap when consumers re-publish from a
   * render path that produces the same string on every tick.
   *
   * Returns `true` when the entry was mutated, `false` otherwise. Callers
   * that want to gate a `console.warn` on no-op invocations can use the
   * return value instead of recomputing equality.
   */
  updateElementMeta(id, meta) {
    const element = this.elements.get(id);
    if (!element) return false;
    const currentIdentifier = element.getIdentifier();
    const nextLabel = meta.label !== void 0 ? meta.label : element.label;
    const nextAccessibilityLabel = meta.accessibilityLabel !== void 0 ? meta.accessibilityLabel : currentIdentifier.accessibilityLabel;
    const nextTestId = meta.testId !== void 0 ? meta.testId : currentIdentifier.testId;
    const labelChanged = nextLabel !== element.label;
    const accessibilityLabelChanged = nextAccessibilityLabel !== currentIdentifier.accessibilityLabel;
    const testIdChanged = nextTestId !== currentIdentifier.testId;
    if (!labelChanged && !accessibilityLabelChanged && !testIdChanged) {
      return false;
    }
    const updated = {
      ...element,
      label: nextLabel,
      getIdentifier: () => ({
        uiId: id,
        testId: nextTestId,
        accessibilityLabel: nextAccessibilityLabel,
        treePath: currentIdentifier.treePath
      })
    };
    this.elements.set(id, updated);
    if (element.type === "text" && labelChanged && typeof nextLabel === "string") {
      this.updateElementState(id, { value: nextLabel });
    }
    this.emit("element:registered", {
      id,
      type: element.type,
      label: nextLabel
    });
    return true;
  }
  /**
   * Update element props (for action execution).
   *
   * Side effect for `type: 'input'` elements: when `props.value` is a string,
   * mirror it into `state.value` so AI drivers reading the registry don't see
   * `state.value === undefined` until the user types. Before this mirror,
   * controlled-input consumers (`<TextInput value={x} onChangeText={setX} />`
   * with `captureProps({ value: x, onChangeText: setX })`) registered an
   * input whose `state.value` only became defined after the FIRST
   * `onChangeText` call — bridge consumers couldn't read the default value
   * (e.g. a pre-populated API-URL input) until someone typed into it.
   *
   * Re-runs on every `updateElementProps` so a parent passing a fresh
   * `value` prop (controlled re-render) keeps the registry in sync. The
   * mirror is gated on `type === 'input'` to avoid touching non-input
   * elements that may carry an unrelated `value` prop (e.g. a Switch's
   * boolean value, which lives on `state.checked`).
   */
  updateElementProps(id, props) {
    const element = this.elements.get(id);
    if (element) {
      const updated = {
        ...element,
        props: { ...element.props, ...props }
      };
      this.elements.set(id, updated);
      if (element.type === "input" && Object.prototype.hasOwnProperty.call(props, "value") && typeof props.value === "string") {
        const currentState = updated.getState();
        if (currentState.value !== props.value) {
          this.updateElementState(id, { value: props.value });
        }
      }
    }
  }
  /**
   * Update element style for design review
   */
  updateElementStyle(id, flatStyle, stateStyles) {
    const element = this.elements.get(id);
    if (element) {
      const updated = {
        ...element,
        flatStyle,
        ...stateStyles !== void 0 ? { stateStyles } : {}
      };
      this.elements.set(id, updated);
    }
  }
  /**
   * Get element style for design review
   */
  getElementStyle(id) {
    const element = this.elements.get(id);
    return element?.flatStyle ?? null;
  }
  /**
   * Find element by testID
   */
  findByTestId(testId) {
    for (const element of this.elements.values()) {
      const identifier = element.getIdentifier();
      if (identifier.testId === testId) {
        return element;
      }
    }
    return void 0;
  }
  /**
   * Find elements by type
   */
  findByType(type) {
    return Array.from(this.elements.values()).filter((e) => e.type === type);
  }
  // ============================================================================
  // Component Management
  // ============================================================================
  /**
   * Register a component
   */
  registerComponent(id, options) {
    const { name, description, actions = [], elementIds } = options;
    const registered = {
      id,
      name,
      description,
      actions: actions.map((a) => ({
        id: a.id,
        label: a.label,
        description: a.description,
        handler: a.handler
      })),
      elementIds,
      registeredAt: Date.now(),
      mounted: true
    };
    this.components.set(id, registered);
    this.emit("component:registered", { id, name });
    if (this.config.verbose) {
      console.log(`[ui-bridge-native] Registered component: ${id} (${name})`);
    }
    return registered;
  }
  /**
   * Unregister a component
   */
  unregisterComponent(id) {
    const component = this.components.get(id);
    if (component) {
      this.components.delete(id);
      this.emit("component:unregistered", { id });
      if (this.config.verbose) {
        console.log(`[ui-bridge-native] Unregistered component: ${id}`);
      }
    }
  }
  /**
   * Get a registered component
   */
  getComponent(id) {
    return this.components.get(id);
  }
  /**
   * Get all registered components
   */
  getAllComponents() {
    return Array.from(this.components.values());
  }
  // ============================================================================
  // Workflow Management
  // ============================================================================
  /**
   * Register a workflow
   */
  registerWorkflow(workflow) {
    this.workflows.set(workflow.id, workflow);
    if (this.config.verbose) {
      console.log(`[ui-bridge-native] Registered workflow: ${workflow.id}`);
    }
  }
  /**
   * Unregister a workflow
   */
  unregisterWorkflow(id) {
    this.workflows.delete(id);
  }
  /**
   * Get a workflow
   */
  getWorkflow(id) {
    return this.workflows.get(id);
  }
  /**
   * Get all workflows
   */
  getAllWorkflows() {
    return Array.from(this.workflows.values());
  }
  // ============================================================================
  // Event System
  // ============================================================================
  /**
   * Subscribe to events
   */
  on(type, listener) {
    if (!this.eventListeners.has(type)) {
      this.eventListeners.set(type, /* @__PURE__ */ new Set());
    }
    this.eventListeners.get(type).add(listener);
    return () => this.off(type, listener);
  }
  /**
   * Unsubscribe from events
   */
  off(type, listener) {
    this.eventListeners.get(type)?.delete(listener);
  }
  /**
   * Emit an event
   */
  emit(type, data) {
    const event = {
      type,
      timestamp: Date.now(),
      data
    };
    const listeners = this.eventListeners.get(type);
    if (listeners) {
      for (const listener of listeners) {
        try {
          listener(event);
        } catch (error) {
          console.error(`[ui-bridge-native] Event listener error:`, error);
        }
      }
    }
    if (this.config.onEvent) {
      try {
        this.config.onEvent(event);
      } catch (error) {
        console.error(`[ui-bridge-native] Global event handler error:`, error);
      }
    }
  }
  // ============================================================================
  // Snapshots
  // ============================================================================
  /**
   * Create a snapshot of the current state.
   *
   * Route resolution: `routeInfo` wins when provided. Otherwise — and this
   * is the path the default `getSnapshot` HTTP handler takes — we fall back
   * to the registered `routeProvider`. This means callers that never wire a
   * server-level override (the bare-server / cloud-relay path) still get a
   * populated `currentRoute` field as long as `setRouteProvider` was called
   * on the registry.
   */
  createSnapshot(routeInfo, options) {
    const resolvedRoute = (() => {
      if (routeInfo !== void 0) {
        return {
          currentRoute: routeInfo.currentRoute ?? null,
          segments: routeInfo.segments
        };
      }
      if (this.routeProvider) {
        try {
          return {
            currentRoute: this.routeProvider.getCurrentRoute() ?? null,
            segments: this.routeProvider.getSegments?.()
          };
        } catch (error) {
          if (this.config.verbose) {
            console.warn(`[ui-bridge-native] routeProvider getCurrentRoute threw:`, error);
          }
          return { currentRoute: null, segments: void 0 };
        }
      }
      return { currentRoute: null, segments: void 0 };
    })();
    let elements = options?.visibleOnly ? this.getMountedVisibleElements() : this.getAllElements();
    if (options?.currentRouteOnly && resolvedRoute.currentRoute) {
      const currentRoute = resolvedRoute.currentRoute;
      elements = elements.filter((e) => matchesCurrentRoute(e.registrationRoute, currentRoute));
    }
    const snapshot = {
      timestamp: Date.now(),
      elements: elements.map((e) => {
        const handlers = extractHandlerNames(e.props);
        const state = e.getState();
        const visibility = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
        const bbox = projectBbox(state, visibility, this.pixelRatio);
        const vision = projectVisionFields({
          type: e.type,
          label: e.label,
          // Real interactivity signal: the names of props that are actually
          // functions (onPress/onChangeText/...). NOT `e.actions`, which
          // `inferActions` synthesizes press/click onto for every element —
          // using it would mark plain text/view nodes interactable.
          handlerNames: handlers,
          state,
          flatStyle: e.flatStyle
        });
        return {
          id: e.id,
          type: e.type,
          label: e.label,
          identifier: e.getIdentifier(),
          state,
          actions: e.actions,
          customActions: e.customActions ? Object.keys(e.customActions) : void 0,
          registeredHandlers: handlers.length > 0 ? handlers : void 0,
          registrationRoute: e.registrationRoute,
          visibility,
          ...bbox ? { bbox } : {},
          ...vision
        };
      }),
      components: this.getAllComponents().map((c) => ({
        id: c.id,
        name: c.name,
        description: c.description,
        actions: c.actions.map((a) => a.id),
        elementIds: c.elementIds
      })),
      workflows: this.getAllWorkflows().map((w) => ({
        id: w.id,
        name: w.name,
        description: w.description,
        stepCount: w.steps.length
      })),
      currentRoute: resolvedRoute.currentRoute,
      segments: resolvedRoute.segments,
      registration: this.getRegistrationCoverage()
    };
    if (this.appInfo) {
      snapshot.appInfo = this.appInfo;
    }
    if (this.enrichers.modalDetector) {
      try {
        snapshot.modalStack = this.enrichers.modalDetector.getSnapshotModalContext();
      } catch (error) {
        if (this.config.verbose) {
          console.warn(`[ui-bridge-native] modalDetector enricher threw:`, error);
        }
      }
    }
    if (this.enrichers.toastCapture) {
      try {
        snapshot.toasts = this.enrichers.toastCapture.getSnapshotToastContext();
      } catch (error) {
        if (this.config.verbose) {
          console.warn(`[ui-bridge-native] toastCapture enricher threw:`, error);
        }
      }
    }
    if (this.enrichers.undoTracker) {
      try {
        snapshot.undoRedo = this.enrichers.undoTracker.getSnapshotUndoContext();
      } catch (error) {
        if (this.config.verbose) {
          console.warn(`[ui-bridge-native] undoTracker enricher threw:`, error);
        }
      }
    }
    if (this.snapshotExtras.size > 0) {
      const ctx = { elements, currentRoute: resolvedRoute.currentRoute };
      for (const [name, fn] of this.snapshotExtras) {
        try {
          const extra = fn(ctx);
          if (extra && typeof extra === "object") {
            Object.assign(snapshot, extra);
          }
        } catch (error) {
          if (this.config.verbose) {
            console.warn(`[ui-bridge-native] snapshot enricher "${name}" threw:`, error);
          }
        }
      }
    }
    return snapshot;
  }
  /**
   * Get registry statistics
   */
  getStats() {
    return {
      elements: this.elements.size,
      components: this.components.size,
      workflows: this.workflows.size
    };
  }
  /**
   * Compute registration coverage metadata for the current registry state.
   *
   * Groups currently-registered elements by `registrationRoute`, bucketing
   * elements without a route under `'?'`. `everHadRegistrations` is sticky —
   * once true, it stays true for the lifetime of the registry instance.
   */
  getRegistrationCoverage() {
    const byRoute = {};
    for (const element of this.elements.values()) {
      const key = element.registrationRoute == null || element.registrationRoute === "" ? "?" : element.registrationRoute;
      byRoute[key] = (byRoute[key] ?? 0) + 1;
    }
    return {
      totalRegistered: this.elements.size,
      everHadRegistrations: this.everHadRegistrations,
      byRoute
    };
  }
  /**
   * Clear all registrations
   */
  clear() {
    this.elements.clear();
    this.components.clear();
    this.workflows.clear();
    if (this.config.verbose) {
      console.log(`[ui-bridge-native] Registry cleared`);
    }
  }
};
var globalRegistry = null;
function setGlobalRegistry(registry) {
  globalRegistry = registry;
}
function getGlobalRegistry() {
  return globalRegistry;
}
function resetGlobalRegistry() {
  globalRegistry?.clear();
  globalRegistry = null;
}

// src/core/element-identifier.ts
function createNativeElementIdentifier(id, options = {}) {
  return {
    uiId: id,
    testId: options.testId || id,
    accessibilityLabel: options.accessibilityLabel,
    accessibilityHint: options.accessibilityHint,
    treePath: options.treePath || id
  };
}
function findElementByIdentifier(identifier) {
  const registry = getGlobalRegistry();
  if (!registry) return null;
  if (typeof identifier === "string") {
    const byId = registry.getElement(identifier);
    if (byId) return byId;
    const byTestId = registry.findByTestId(identifier);
    if (byTestId) return byTestId;
    return findByPattern(registry, identifier);
  }
  if (identifier.uiId) {
    const byId = registry.getElement(identifier.uiId);
    if (byId) return byId;
  }
  if (identifier.testId) {
    const byTestId = registry.findByTestId(identifier.testId);
    if (byTestId) return byTestId;
  }
  return null;
}
function findByPattern(registry, pattern) {
  if (!registry) return null;
  const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
  const regex = new RegExp(`^${regexPattern}$`, "i");
  for (const element of registry.getAllElements()) {
    const identifier = element.getIdentifier();
    if (identifier.testId && regex.test(identifier.testId)) {
      return element;
    }
    if (identifier.uiId && regex.test(identifier.uiId)) {
      return element;
    }
    if (identifier.treePath && regex.test(identifier.treePath)) {
      return element;
    }
    if (identifier.accessibilityLabel && regex.test(identifier.accessibilityLabel)) {
      return element;
    }
  }
  return null;
}
function findAllByPattern(pattern) {
  const registry = getGlobalRegistry();
  if (!registry) return [];
  const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
  const regex = new RegExp(`^${regexPattern}$`, "i");
  const results = [];
  for (const element of registry.getAllElements()) {
    const identifier = element.getIdentifier();
    if (identifier.testId && regex.test(identifier.testId) || identifier.uiId && regex.test(identifier.uiId) || identifier.treePath && regex.test(identifier.treePath) || identifier.accessibilityLabel && regex.test(identifier.accessibilityLabel)) {
      results.push(element);
    }
  }
  return results;
}
function buildTreePath(componentPath, elementIndex) {
  let path = componentPath.join("/");
  if (elementIndex !== void 0) {
    path += `[${elementIndex}]`;
  }
  return path;
}
function parseTreePath(treePath) {
  const indexMatch = treePath.match(/\[(\d+)\]$/);
  const index = indexMatch ? parseInt(indexMatch[1], 10) : void 0;
  const pathWithoutIndex = treePath.replace(/\[\d+\]$/, "");
  const components = pathWithoutIndex.split("/").filter(Boolean);
  return { components, index };
}
function matchesIdentifier(identifier, criteria) {
  if (criteria.uiId && identifier.uiId !== criteria.uiId) {
    return false;
  }
  if (criteria.testId && identifier.testId !== criteria.testId) {
    return false;
  }
  if (criteria.accessibilityLabel && identifier.accessibilityLabel !== criteria.accessibilityLabel) {
    return false;
  }
  if (criteria.treePath && identifier.treePath !== criteria.treePath) {
    return false;
  }
  return true;
}

export { NativeUIBridgeRegistry, buildTreePath, createNativeElementIdentifier, extractHandlerNames, findAllByPattern, findElementByIdentifier, getGlobalRegistry, matchesCurrentRoute, matchesIdentifier, parseTreePath, projectBbox, resetGlobalRegistry, setGlobalRegistry };
//# sourceMappingURL=index.mjs.map
//# sourceMappingURL=index.mjs.map