> ## Documentation Index
> Fetch the complete documentation index at: https://lationscripts.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Interact - Zones

> Sphere and box zones - interaction anchors whose reach is a volume, not a distance

A zone is an anchor whose reach is a **volume**: instead of standing within a distance of a point, the player walks *inside* the shape and the stack is live. Register a sphere with `addSphere` or an (optionally rotated) box with `addBox` - everything else works exactly like a [point](/docs/interact/api/points): the same [reveal stages](/docs/interact/how-it-works), the same [options and condition gates](/docs/interact/api/options), the same automatic cleanup when your resource stops.

<Note>
  Zones live in the same spatial grid as points, and the frame loop only ever looks at the player's neighborhood - so thousands of registered zones cost nothing at idle. Register your whole map up front; there's no need to create and destroy zones as players move around.
</Note>

## Quick Start

```lua theme={null}
local fire = exports.lation_interact:addSphere({
    coords = vector3(-1153.4, 4906.1, 221.3),
    radius = 2.0,
    label = 'Campfire',
    icon = 'fire',
    options = {
        {
            label = 'Warm Up',
            icon = 'hand-holding-heart',
            onSelect = function(data)
                -- play the warm-up scenario
            end
        },
        {
            label = 'Cook Meat',
            icon = 'drumstick-bite',
            items = 'raw_meat',
            hold = 3000,
            serverEvent = 'camp:cook'
        }
    }
})
```

The radius *is* the reach: anywhere inside those `2.0m` the stack is live. The name and grain stages derive from the radius automatically - see [Stage Derivation](#stage-derivation).

## Functions

<AccordionGroup>
  <Accordion title="addSphere" icon="circle-dot">
    Register a sphere zone: a point whose reach is the sphere itself.

    ```lua theme={null}
    local id = exports.lation_interact:addSphere(data)
    ```

    **Parameters:**

    * `data` (table) - Zone configuration (see [Sphere Options](#sphere-options))

    **Returns:**

    * `id` (number) - The zone's anchor id, for `remove` - or `nil` if the data was invalid (the reason is printed to the client console)
  </Accordion>

  <Accordion title="addBox" icon="cube">
    Register a box zone: reach is being *inside* the (optionally rotated) box.

    ```lua theme={null}
    local id = exports.lation_interact:addBox(data)
    ```

    **Parameters:**

    * `data` (table) - Zone configuration (see [Box Options](#box-options))

    **Returns:**

    * `id` (number) - The zone's anchor id, for `remove` - or `nil` if the data was invalid (the reason is printed to the client console)
  </Accordion>

  <Accordion title="remove" icon="trash">
    Remove an anchor by id - the same `remove` shared by every `add*` export.

    ```lua theme={null}
    exports.lation_interact:remove(id)
    ```

    **Parameters:**

    * `id` (number) - The id returned at registration

    <Note>
      Manual removal is only needed for zones that stop making sense mid-session. Everything your resource registered is removed automatically when it stops or restarts.
    </Note>
  </Accordion>
</AccordionGroup>

## Configuration

### Sphere Options

<ParamField path="coords" type="vector3" required>
  Center of the sphere. A `vector4` or a plain `{ x, y, z }` table (JSON, database rows) is accepted too
</ParamField>

<ParamField path="radius" type="number" required>
  Radius of the sphere in meters (must be greater than `0`) - this is the reach: the stack is live while the player is inside it
</ParamField>

<ParamField path="reachMargin" type="number" default="0.0">
  Extend the reach this far *outside* the radius. Useful for a small sphere on a prop or the floor, which the player's origin - about a meter up - can never enter. The compatibility layers use it for bridged zones
</ParamField>

### Box Options

<ParamField path="coords" type="vector3" required>
  Center of the box - the volume extends `size / 2` in every direction from here, including vertically, so place `coords.z` at the vertical center of the volume, not the floor. A `vector4` or a plain `{ x, y, z }` table is accepted too
</ParamField>

<ParamField path="size" type="vector3" required>
  Full dimensions of the box in meters: `x` width, `y` depth, `z` height (a plain `{ x, y, z }` table is accepted too)
</ParamField>

<ParamField path="rotation" type="number" default="0.0">
  Rotation of the box in degrees around the vertical axis (a heading). A `vector3` euler or a `vector4` quaternion is accepted too - its yaw is used
</ParamField>

<ParamField path="reachMargin" type="number" default="0.0">
  Extend the reach this far outside the box in every direction. The compatibility layers use it for bridged zones
</ParamField>

### Shared Fields

Both exports accept the same anchor-level fields as every other `add*` export: `label`, `icon`, `options` (required), `stages`, `expand`, `reveal`, `passive`, and `inVehicle`, plus `debug` (see [Debugging](#debugging)). They are documented in full on [Options](/docs/interact/api/options). Two zone-specific notes:

* Zones default to `reveal = 'ambient'`, like points.
* `stages` overrides the [derived stages](#stage-derivation) below; any key you omit keeps its derived value.

<Warning>
  The `state` statebag gate applies to entity anchors only. A zone has no statebag, so a `state`-gated option on a zone prints a warning at registration and stays hidden.
</Warning>

## Stage Derivation

Zones derive their [reveal stages](/docs/interact/how-it-works#the-three-stages) from the volume instead of using the `Config.Stages` defaults:

| Stage   | Sphere                          | Box                                                 |
| ------- | ------------------------------- | --------------------------------------------------- |
| `reach` | `radius + reachMargin`          | being **inside** the volume, grown by `reachMargin` |
| `name`  | `max(reach + 1.8, reach × 1.3)` | `boundingRadius + 2.0`                              |
| `grain` | `max(reach + 5.0, reach × 1.8)` | `boundingRadius + 5.0`                              |

`boundingRadius` is the box's horizontal bounding radius: `sqrt((size.x / 2)² + (size.y / 2)²)`. A small zone gets comfortable absolute padding around it; a large one scales proportionally instead, so its grain and name stages always sit outside the volume.

For a box, the `name` and `grain` stages are radial distances from the center, but reach truth is the **inside test**: the stack goes live the moment the player steps into the rotated volume, whatever their distance from the center.

<Note>
  Zones bridged from ox\_target / qb-target don't use these defaults: they reach from `Config.Stages.reach` outside their surface (never farther than the registration's own distance), the way their eye targeted them from a step away. See the [ox\_target](/docs/interact/compat/ox-target) and [qb-target](/docs/interact/compat/qb-target) pages.
</Note>

Stage boundaries are hysteretic everywhere: entering a stage is instant, but leaving requires backing out `Config.Stages.exitMargin` (`0.4m`) past its threshold. For a box this applies to the inside test itself - while the stack is live, the volume is treated as `0.4m` larger in every direction - so standing exactly on the edge never flickers the stack on and off.

## Occlusion

Zones are never hidden by occlusion testing. A zone's center usually sits inside the prop it wraps - a counter, an ATM, a vending machine - so a blocked test would hide it incorrectly.

## Debugging

Set `debug = true` on any zone to draw its physical volume in-world: boxes as translucent faces plus edges, spheres as a sphere of their radius. Shapes are tinted with the theme accent and drawn at any distance while the flag is set - remove it for production.

```lua theme={null}
exports.lation_interact:addBox({
    coords = vector3(24.9, -1346.6, 29.7), -- vertical center of the volume
    size = vector3(2.4, 1.2, 2.0),
    rotation = 90.0,
    label = 'Store Counter',
    icon = 'cash-register',
    debug = true, -- draw the volume while lining it up
    options = {
        {
            label = 'Buy Snacks',
            icon = 'basket-shopping',
            serverEvent = 'shop:open'
        },
        {
            label = 'Rob Register',
            icon = 'gun',
            hold = 5000,
            canInteract = function()
                local hour = GetClockHours()
                return hour >= 22 or hour < 5
            end,
            serverEvent = 'shop:rob'
        }
    }
})
```

Walk around the counter with the volume drawn until the box hugs it, note the final `coords`, `size`, and `rotation`, then drop the `debug` flag.
