{
  "name": "interactable",
  "title": "Interactable",
  "type": "registry:component",
  "description": "Wrap anything to make it something the player can act on: walk within range and a prompt shows its actions with the keys the world binds to them; press one and onAction gets its id. Only the nearest in range takes the key, and keys yield to text fields.",
  "dependencies": [
    "@react-three/drei@^10.7.7",
    "@runek/core@^0.13.0",
    "three@^0.184.0"
  ],
  "registryDependencies": [
    "sign"
  ],
  "files": [
    {
      "path": "Interactable.tsx",
      "content": "import { Billboard } from '@react-three/drei'\nimport {\n  type InteractionAction,\n  keyLabel,\n  useInteraction,\n  useWorld,\n  type Vec3,\n  type WorldComponentProps,\n} from '@runek/core'\nimport { type ReactNode, useRef } from 'react'\nimport type { Group } from 'three'\nimport { Sign } from './Sign'\n\nexport interface InteractableProps extends WorldComponentProps {\n  /** What the player can do here. Each names a world `controls` action for its key, so declare\n   *  it in the world (`controls: { use: ['KeyE'] }`). */\n  actions?: InteractionAction[]\n  /** How close the avatar must come for the prompt to show, in units. */\n  radius?: number\n  /** Height of the prompt above the origin, in units. */\n  promptHeight?: number\n  onAction?: (id: string) => void\n  children?: ReactNode\n}\n\nexport interface InteractionPromptProps {\n  actions: InteractionAction[]\n  position?: Vec3\n  /** Cap height of the text, in units. */\n  size?: number\n}\n\nconst INK = '#f6f1e7'\n\n/** The key-and-label rows over something you can act on: one row per action, its key cap showing\n *  the key the world binds to it (so a remap relabels it). Always faces the camera. */\nexport function InteractionPrompt({ actions, position, size = 0.09 }: InteractionPromptProps) {\n  const { unit, palette, controls } = useWorld()\n  const s = size * unit\n  const row = s * 2.3\n  return (\n    <Billboard position={position}>\n      {actions.map((action, i) => {\n        const code = controls[action.control]?.[0]\n        const key = code ? keyLabel(code) : '?'\n        const cap = Math.max(1, key.length * 0.62) * s + s * 0.9\n        const y = (actions.length - 1 - i) * row\n        return (\n          <group key={action.id} position={[0, y, 0]}>\n            <mesh position={[-cap / 2, 0, 0]} renderOrder={1}>\n              <planeGeometry args={[cap, s * 1.75]} />\n              <meshBasicMaterial color={palette.accent} toneMapped={false} />\n            </mesh>\n            <Sign\n              position={[-cap / 2, 0, 0.002 * unit]}\n              variant=\"body\"\n              size={size}\n              color={palette.metal}\n            >\n              {key}\n            </Sign>\n            <Sign\n              position={[s * 0.6, 0, 0]}\n              variant=\"body\"\n              size={size}\n              color={INK}\n              outline={palette.metal}\n              anchorX=\"left\"\n            >\n              {action.label}\n            </Sign>\n          </group>\n        )\n      })}\n    </Billboard>\n  )\n}\n\n/**\n * Wrap anything to make it something the player can act on: walk within `radius` and a prompt\n * appears over it (`E Use`), press the key and `onAction` gets the action's `id`. Of everything\n * in range only the nearest shows its prompt and takes the key, and keys already yield to text\n * fields on the page. The actions are plain JSON, so a world file can declare them; the callback\n * is the host's. No geometry or colliders of its own.\n *\n * ```tsx\n * <World controls={{ use: ['KeyE'] }}>\n *   <Interactable actions={[{ id: 'sit', label: 'Sit', control: 'use' }]} onAction={sitDown}>\n *     <Chair />\n *   </Interactable>\n * </World>\n * ```\n */\nexport function Interactable({\n  position = [0, 0, 0],\n  rotation = [0, 0, 0],\n  actions = [],\n  radius = 2,\n  promptHeight = 1.4,\n  onAction,\n  children,\n}: InteractableProps) {\n  const { unit } = useWorld()\n  const anchor = useRef<Group>(null)\n  const focused = useInteraction(anchor, { actions, radius, onAction })\n  return (\n    <group ref={anchor} position={position} rotation={rotation}>\n      {children}\n      {focused && <InteractionPrompt actions={actions} position={[0, promptHeight * unit, 0]} />}\n    </group>\n  )\n}\n",
      "type": "registry:component"
    }
  ]
}
