/**
 * Stage 4.6 port of Base UI React's `useTypeahead` hook.
 *
 * React source:
 *   `base-ui/packages/react/src/floating-ui-react/hooks/useTypeahead.ts`
 *
 * Provides a matching callback that focuses / highlights a list item as
 * the user types. Typically paired with `useListNavigation`; the React
 * fork delivers this hook unchanged from upstream `@floating-ui/react`.
 *
 * ### React → Solid deviations
 *
 * 1. `useRef` → `MutableRef<T>` (matches sibling hooks). Closed-over
 *    `{ current }` containers hold the typeahead string, the last matched
 *    index, and the previous-index snapshot.
 *
 * 2. `useStableCallback` → `createStableCallback` from
 *    `src/internals/stable-callback.ts`. Used only where React stabilises
 *    identity for effect deps / memo keys — here it stabilises the
 *    event handlers so they can be captured in `reference` / `floating`
 *    bags without identity churn.
 *
 * 3. `useIsoLayoutEffect` (with React single-callback + deps) →
 *    `createRenderEffect`-backed `useIsoLayoutEffect(compute, apply)` in
 *    split form. Tracked reads go in the `compute` factory; the
 *    side-effect body runs in `apply`. We use `store.state.open`
 *    inside `compute` to subscribe and `store.state.open` inside `apply`
 *    for the sync read.
 *
 * 4. `useMemo` for `reference` / `floating` bags collapses to plain
 *    object literals — Solid doesn't re-run component bodies, so the bag
 *    identity is already stable for the lifetime of the call site.
 *
 * 5. Event handlers receive native events directly (no `.nativeEvent`
 *    unwrap).
 *
 * 6. `MaybeAccessor<T>` applies to the reactive option fields
 *    (`activeIndex`, `selectedIndex`, `enabled`, `resetMs`) so consumers
 *    that mount the hook once can still flip values later — same
 *    convention as `useListNavigation`. Literal values also work.
 *
 * 7. Shadow-DOM-safe `contains()` from `src/internals/dom/contains.ts`
 *    replaces raw `node.contains(target)`.
 *
 * ### Reason strings
 *
 * `useTypeahead` does not call `store.setOpen` — it only invokes
 * `onMatch(index)` and the `onTypingChange(isTyping)` callback. No
 * `REASONS.*` strings are emitted from this hook. Matches React 1:1.
 */

import { untrack } from "solid-js";
import { contains } from "../../dom/contains.jsx";
import { readOption } from "../../maybe-accessor.jsx";
import { createStableCallback } from "../../stable-callback.jsx";
import { useIsoLayoutEffect } from "../../use-iso-layout-effect.jsx";
import { useTimeout } from "../../use-timeout.jsx";
import { isElementVisible } from "../utils/composite.jsx";
import { stopEvent } from "../utils/event.jsx";
/**
 * Provides a matching callback that can be used to focus an item as the
 * user types, often used in tandem with `useListNavigation()`.
 * @see https://floating-ui.com/docs/useTypeahead
 */
export function useTypeahead(context, props) {
  const store = "rootStore" in context ? context.rootStore : context;
  const dataRef = store.context.dataRef;

  // Untrack the body-time destructure (see use-dismiss for the full
  // reasoning). These fields are not `MaybeAccessor` shapes.
  const {
    listRef,
    elementsRef,
    onMatch: onMatchProp,
    onTypingChange
  } = untrack(() => ({
    listRef: props.listRef,
    elementsRef: props.elementsRef,
    onMatch: props.onMatch,
    onTypingChange: props.onTypingChange
  }));

  // Option readers — normalise literal + accessor shapes.
  const getActiveIndex = () => readOption(props.activeIndex);
  const getEnabled = () => props.enabled === undefined ? true : readOption(props.enabled);
  const getResetMs = () => props.resetMs === undefined ? 750 : readOption(props.resetMs);
  const getSelectedIndex = () => {
    const v = props.selectedIndex;
    if (v === undefined) return null;
    const read = readOption(v);
    return read ?? null;
  };
  const timeout = useTimeout();
  const stringRef = {
    current: ""
  };
  // One-shot seed: untrack so the body-time strict-read scope
  // established by `devComponent(...)` does not warn. Reactivity for
  // later updates flows through the `useIsoLayoutEffect` below.
  const prevIndexRef = {
    current: untrack(() => getSelectedIndex() ?? getActiveIndex() ?? -1)
  };
  const matchIndexRef = {
    current: null
  };

  // Reset the typeahead accumulator when the floating element closes.
  // React's effect dep array is `[open, selectedIndex, timeout]` — the
  // Solid split form tracks `open` + `selectedIndex` in `compute`; the
  // `timeout` handle identity is stable for the owned scope's lifetime
  // (see `use-timeout.ts`) so we omit it from tracking.
  useIsoLayoutEffect(() => ({
    open: store.state.open,
    selectedIndex: getSelectedIndex()
  }), snapshot => {
    const {
      open,
      selectedIndex
    } = snapshot;
    if (!open && selectedIndex !== null) {
      return;
    }
    timeout.clear();
    matchIndexRef.current = null;
    if (stringRef.current !== "") {
      stringRef.current = "";
    }
  });

  // Sync arrow-key navigation but not typeahead navigation — update the
  // `prevIndexRef` snapshot only when there's no active typing session.
  useIsoLayoutEffect(() => ({
    open: store.state.open,
    selectedIndex: getSelectedIndex(),
    activeIndex: getActiveIndex()
  }), snapshot => {
    const {
      open,
      selectedIndex,
      activeIndex
    } = snapshot;
    if (open && stringRef.current === "") {
      prevIndexRef.current = selectedIndex ?? activeIndex ?? -1;
    }
  });
  const setTypingChange = createStableCallback(() => value => {
    if (value) {
      if (!dataRef.current.typing) {
        dataRef.current.typing = value;
        onTypingChange?.(value);
      }
    } else if (dataRef.current.typing) {
      dataRef.current.typing = value;
      onTypingChange?.(value);
    }
  });
  const onKeyDown = createStableCallback(() => event => {
    if (!getEnabled()) return;
    function isVisible(index) {
      const element = elementsRef?.current[index];
      return !element || isElementVisible(element);
    }
    function getMatchingIndex(list, string, startIndex = 0) {
      if (list.length === 0) {
        return -1;
      }
      const normalizedStartIndex = (startIndex % list.length + list.length) % list.length;
      const lowerString = string.toLocaleLowerCase();
      for (let offset = 0; offset < list.length; offset += 1) {
        const index = (normalizedStartIndex + offset) % list.length;
        const text = list[index];
        if (!text?.toLocaleLowerCase().startsWith(lowerString) || !isVisible(index)) {
          continue;
        }
        return index;
      }
      return -1;
    }
    const listContent = listRef.current;
    const activeIndex = getActiveIndex();
    const selectedIndex = getSelectedIndex();
    const resetMs = getResetMs();
    if (stringRef.current.length > 0 && event.key === " ") {
      // Space should continue the in-progress typeahead session.
      stopEvent(event);
      setTypingChange(true);
    }
    if (stringRef.current.length > 0 && stringRef.current[0] !== " ") {
      if (getMatchingIndex(listContent, stringRef.current) === -1 && event.key !== " ") {
        setTypingChange(false);
      }
    }
    if (listContent == null ||
    // Character key.
    event.key.length !== 1 ||
    // Modifier key.
    event.ctrlKey || event.metaKey || event.altKey) {
      return;
    }
    const open = store.state.open;
    if (open && event.key !== " ") {
      stopEvent(event);
      setTypingChange(true);
    }

    // Capture whether this is a new typing session before mutating
    // the string.
    const isNewSession = stringRef.current === "";
    if (isNewSession) {
      prevIndexRef.current = selectedIndex ?? activeIndex ?? -1;
    }

    // Bail out if the list contains a word like "llama" or "aaron".
    // TODO: allow it in this case, too.
    const allowRapidSuccessionOfFirstLetter = listContent.every(text => text ? text[0]?.toLocaleLowerCase() !== text[1]?.toLocaleLowerCase() : true);

    // Allows the user to cycle through items that start with the
    // same letter in rapid succession.
    if (allowRapidSuccessionOfFirstLetter && stringRef.current === event.key) {
      stringRef.current = "";
      prevIndexRef.current = matchIndexRef.current;
    }
    stringRef.current += event.key;
    timeout.start(resetMs, () => {
      stringRef.current = "";
      prevIndexRef.current = matchIndexRef.current;
      setTypingChange(false);
    });

    // Compute the starting index for this search. For a new session,
    // base it on the current selection/active item; otherwise
    // continue from the last matched index.
    const prevIndex = isNewSession ? selectedIndex ?? activeIndex ?? -1 : prevIndexRef.current;
    const startIndex = (prevIndex ?? 0) + 1;
    const index = getMatchingIndex(listContent, stringRef.current, startIndex);
    if (index !== -1) {
      onMatchProp?.(index);
      matchIndexRef.current = index;
    } else if (event.key !== " ") {
      stringRef.current = "";
      setTypingChange(false);
    }
  });
  const onBlur = createStableCallback(() => event => {
    if (!getEnabled()) return;
    const next = event.relatedTarget;
    const currentDomReferenceElement = store.state.domReferenceElement;
    const currentFloatingElement = store.state.floatingElement;
    const withinReference = contains(currentDomReferenceElement, next);
    const withinFloating = contains(currentFloatingElement, next);

    // Keep the session if focus moves within the composite
    // (reference <-> floating).
    if (withinReference || withinFloating) {
      return;
    }

    // End the current typing session when focus leaves the composite.
    timeout.clear();
    stringRef.current = "";
    prevIndexRef.current = matchIndexRef.current;
    setTypingChange(false);
  });
  const reference = {
    onKeyDown,
    onBlur
  };
  const floating = {
    onKeyDown,
    onBlur
  };

  // Signalled enablement: when `enabled` is a literal `false`, return
  // `{}` to match React. When it's an accessor, always return the bag
  // so handlers can short-circuit internally — matches the hover /
  // list-navigation enablement contract. Untrack the body-time check
  // so the strict-read scope established by `devComponent(...)` does
  // not warn — handlers re-read the accessor at event time inside
  // already-tracked scopes.
  const isStaticallyDisabled = untrack(() => typeof props.enabled !== "function" && !getEnabled());
  if (isStaticallyDisabled) {
    return {};
  }
  return {
    reference,
    floating
  };
}
//# sourceMappingURL=use-typeahead.jsx.map
