{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ink/interaction",
  "title": "Interaction",
  "description": "Focus-gated Ink input handling and modal focus scopes.",
  "dependencies": [
    "ink"
  ],
  "files": [
    {
      "path": "registry/hooks/use-interaction.tsx",
      "content": "import { useFocus, useFocusManager, useInput, useStdout } from \"ink\";\nimport type { Key } from \"ink\";\nimport * as React from \"react\";\n\nexport type { Key } from \"ink\";\n\nexport interface InteractionProps {\n  id?: string;\n  autoFocus?: boolean;\n  isActive?: boolean;\n  disabled?: boolean;\n}\n\nexport interface UseInteractionOptions extends InteractionProps {\n  onInput?: (input: string, key: Key) => void;\n}\n\nexport interface UseInteractionResult {\n  isFocused: boolean;\n  focus: () => void;\n  id: string;\n}\n\ninterface RegisteredControl {\n  disabled: boolean;\n  id: string;\n}\n\ninterface FocusScopeContextValue {\n  active: boolean;\n  focus: (id: string) => void;\n  focusedId?: string;\n  isTopmost: boolean;\n  register: (control: RegisteredControl) => () => void;\n  setNestedActive: (scopeId: string, active: boolean) => void;\n}\n\nconst FocusScopeContext = React.createContext<FocusScopeContextValue | null>(\n  null\n);\n\nconst lastFocusedByStdout = new WeakMap<object, string>();\nconst activeScopesByStdout = new WeakMap<object, Set<string>>();\n\nconst getEnabledControlIds = (controls: readonly RegisteredControl[]) =>\n  controls.filter(({ disabled }) => !disabled).map(({ id }) => id);\n\nexport interface FocusScopeProps {\n  active: boolean;\n  initialFocusId?: string;\n  returnFocusId?: string;\n  loop?: boolean;\n  onEscapeKey?: () => void;\n  children: React.ReactNode;\n}\n\nexport const FocusScope = ({\n  active,\n  initialFocusId,\n  returnFocusId,\n  loop = true,\n  onEscapeKey,\n  children,\n}: FocusScopeProps) => {\n  const generatedId = React.useId();\n  const scopeId = `termcn-scope-${generatedId}`;\n  const parentScope = React.useContext(FocusScopeContext);\n  const parentIsActive = parentScope?.active ?? false;\n  const setParentNestedActive = parentScope?.setNestedActive;\n  const { stdout } = useStdout();\n  const { disableFocus, enableFocus, focus: focusById } = useFocusManager();\n  const controlsRef = React.useRef<RegisteredControl[]>([]);\n  const nestedScopesRef = React.useRef(new Set<string>());\n  const restoreFocusIdRef = React.useRef<string | null>(null);\n  const [focusedId, setFocusedId] = React.useState<string>();\n  const [registrationVersion, setRegistrationVersion] = React.useState(0);\n  const [nestedScopeCount, setNestedScopeCount] = React.useState(0);\n  const isTopmost = nestedScopeCount === 0;\n\n  const register = React.useCallback((control: RegisteredControl) => {\n    const existingIndex = controlsRef.current.findIndex(\n      ({ id }) => id === control.id\n    );\n    if (existingIndex === -1) {\n      controlsRef.current.push(control);\n    } else {\n      controlsRef.current[existingIndex] = control;\n    }\n    setRegistrationVersion((version) => version + 1);\n\n    return () => {\n      controlsRef.current = controlsRef.current.filter(\n        ({ id }) => id !== control.id\n      );\n      setRegistrationVersion((version) => version + 1);\n    };\n  }, []);\n\n  const setNestedActive = React.useCallback(\n    (nestedScopeId: string, nestedActive: boolean) => {\n      if (nestedActive) {\n        nestedScopesRef.current.add(nestedScopeId);\n      } else {\n        nestedScopesRef.current.delete(nestedScopeId);\n      }\n      setNestedScopeCount(nestedScopesRef.current.size);\n    },\n    []\n  );\n\n  React.useEffect(() => {\n    if (!(active && parentIsActive && setParentNestedActive)) {\n      return;\n    }\n\n    setParentNestedActive(scopeId, true);\n    return () => setParentNestedActive(scopeId, false);\n  }, [active, parentIsActive, scopeId, setParentNestedActive]);\n\n  React.useEffect(() => {\n    if (!active) {\n      return;\n    }\n\n    if (parentIsActive) {\n      return;\n    }\n\n    restoreFocusIdRef.current =\n      returnFocusId ?? lastFocusedByStdout.get(stdout) ?? null;\n    disableFocus();\n\n    const activeScopes = activeScopesByStdout.get(stdout) ?? new Set<string>();\n    if (\n      process.env.NODE_ENV !== \"production\" &&\n      activeScopes.size > 0 &&\n      !activeScopes.has(scopeId)\n    ) {\n      console.warn(\n        \"termcn Ink: multiple non-nested focus scopes are active; only nested overlays are supported.\"\n      );\n    }\n    activeScopes.add(scopeId);\n    activeScopesByStdout.set(stdout, activeScopes);\n\n    return () => {\n      activeScopes.delete(scopeId);\n      if (activeScopes.size === 0) {\n        activeScopesByStdout.delete(stdout);\n      }\n\n      enableFocus();\n      const restoreId = returnFocusId ?? restoreFocusIdRef.current;\n      if (restoreId) {\n        focusById(restoreId);\n      }\n    };\n  }, [\n    active,\n    disableFocus,\n    enableFocus,\n    focusById,\n    parentIsActive,\n    returnFocusId,\n    scopeId,\n    stdout,\n  ]);\n\n  React.useEffect(() => {\n    if (!active) {\n      setFocusedId(undefined);\n      return;\n    }\n\n    const enabledIds = getEnabledControlIds(controlsRef.current);\n    if (enabledIds.length === 0) {\n      setFocusedId(undefined);\n      return;\n    }\n\n    setFocusedId((currentId) => {\n      if (currentId && enabledIds.includes(currentId)) {\n        return currentId;\n      }\n\n      return initialFocusId && enabledIds.includes(initialFocusId)\n        ? initialFocusId\n        : enabledIds[0];\n    });\n  }, [active, initialFocusId, registrationVersion]);\n\n  useInput(\n    (input, key) => {\n      if (key.eventType === \"release\") {\n        return;\n      }\n\n      if (key.escape) {\n        onEscapeKey?.();\n        return;\n      }\n\n      if (!(key.tab || input === \"\\t\")) {\n        return;\n      }\n\n      const enabledIds = getEnabledControlIds(controlsRef.current);\n      if (enabledIds.length === 0) {\n        return;\n      }\n\n      const currentIndex = focusedId ? enabledIds.indexOf(focusedId) : -1;\n      const direction = key.shift ? -1 : 1;\n      let nextIndex = currentIndex + direction;\n\n      nextIndex = loop\n        ? (nextIndex + enabledIds.length) % enabledIds.length\n        : Math.max(0, Math.min(nextIndex, enabledIds.length - 1));\n\n      setFocusedId(enabledIds[nextIndex]);\n    },\n    { isActive: active && isTopmost }\n  );\n\n  const contextValue = React.useMemo<FocusScopeContextValue>(\n    () => ({\n      active,\n      focus: setFocusedId,\n      focusedId,\n      isTopmost,\n      register,\n      setNestedActive,\n    }),\n    [active, focusedId, isTopmost, register, setNestedActive]\n  );\n\n  return (\n    <FocusScopeContext.Provider value={contextValue}>\n      {children}\n    </FocusScopeContext.Provider>\n  );\n};\n\nexport function useInteraction(\n  options: UseInteractionOptions\n): UseInteractionResult;\nexport function useInteraction(\n  onInput: (input: string, key: Key) => void,\n  options?: InteractionProps\n): UseInteractionResult;\nexport function useInteraction(\n  optionsOrHandler: UseInteractionOptions | ((input: string, key: Key) => void),\n  legacyOptions: InteractionProps = {}\n): UseInteractionResult {\n  const options =\n    typeof optionsOrHandler === \"function\"\n      ? { ...legacyOptions, onInput: optionsOrHandler }\n      : optionsOrHandler;\n  const {\n    id: providedId,\n    autoFocus = false,\n    isActive = true,\n    disabled = false,\n    onInput,\n  } = options;\n  const reactId = React.useId();\n  const id = providedId ?? `termcn-control-${reactId}`;\n  const scope = React.useContext(FocusScopeContext);\n  const scopeRegister = scope?.register;\n  const { stdout } = useStdout();\n  const enabled = isActive && !disabled;\n  const nativeFocus = useFocus({\n    autoFocus: scope ? false : autoFocus,\n    id,\n    isActive: scope ? false : enabled,\n  });\n  const isFocused = scope\n    ? scope.active && scope.isTopmost && scope.focusedId === id\n    : nativeFocus.isFocused;\n\n  React.useEffect(() => {\n    if (!scopeRegister) {\n      return;\n    }\n\n    return scopeRegister({ disabled: !enabled, id });\n  }, [enabled, id, scopeRegister]);\n\n  React.useEffect(() => {\n    if (isFocused && !scope) {\n      lastFocusedByStdout.set(stdout, id);\n    }\n  }, [id, isFocused, scope, stdout]);\n\n  useInput(\n    (input, key) => {\n      if (key.eventType === \"release\") {\n        return;\n      }\n      onInput?.(input, key);\n    },\n    { isActive: enabled && (scope ? isFocused : true) && Boolean(onInput) }\n  );\n\n  const focus = React.useCallback(() => {\n    if (!enabled) {\n      return;\n    }\n\n    if (scope) {\n      scope.focus(id);\n    } else {\n      nativeFocus.focus(id);\n    }\n  }, [enabled, id, nativeFocus, scope]);\n\n  return { focus, id, isFocused };\n}\n\nexport const isActivationKey = (input: string, key: Key): boolean =>\n  key.eventType !== \"release\" && (key.return || input === \" \");\n",
      "type": "registry:hook"
    }
  ],
  "categories": [
    "core"
  ],
  "type": "registry:hook"
}