# `Mob.Renderer`
[🔗](https://github.com/genericjam/mob/blob/master/lib/mob/renderer.ex#L1)

Serializes a component tree to JSON and passes it to the platform NIF in
a single call. Compose (Android) and SwiftUI (iOS) handle diffing and
rendering internally.

## Node format

    %{
      type: :column,
      props: %{padding: :space_md, background: :surface},
      children: [
        %{type: :text,   props: %{text: "Hello", text_size: :xl, text_color: :on_surface}, children: []},
        %{type: :button, props: %{text: "Tap", on_tap: self()},    children: []}
      ]
    }

## Token resolution

Atom values for color props, spacing props, radius props, and text sizes are
resolved at render time through the active `Mob.Theme` and the base palette.

**Color props** (`:background`, `:text_color`, `:border_color`, `:color`,
`:placeholder_color`): resolved via theme semantic tokens first, then the
base palette. E.g. `:primary` → theme's primary → `:blue_500` → `0xFF2196F3`.

**Spacing props** (`:padding`, `:padding_top`, etc., `:gap`, `:spacing`,
`:run_spacing`): accept spacing
tokens (`:space_xs`, `:space_sm`, `:space_md`, `:space_lg`, `:space_xl`)
that are scaled by `theme.space_scale`.

**Radius props** (`:corner_radius`): accept `:radius_sm`, `:radius_md`,
`:radius_lg`, `:radius_pill` from the active theme.

**Border** (currently honored on `:box` only): set both `:border_color`
(a color token like `:primary` or `:border`) and `:border_width` (an
integer pt/dp value, e.g. `1`). When width is 0 or color is unset, no
border draws.

**Text size props** (`:text_size`, `:font_size`): token atoms (`:xl`, `:lg`,
etc.) are multiplied by `theme.type_scale`.

**Font props** (`:font`): an atom (`:heading`) resolves through the active
theme's `fonts` map to a platform-specific family name; an unresolved atom
or a raw string passes through unchanged. When the theme declares a
`:default` font token, any node without its own `font:` gets it injected
automatically — see `Mob.Theme`'s "Font tokens" section.

## Component defaults

When a component's props omit styling keys, the renderer injects sensible
defaults from the active theme. Explicit props always win over defaults.

    # Gets primary background, white text, medium radius automatically:
    %{type: :button, props: %{text: "Save", on_tap: {self(), :save}}, children: []}

## Style structs

A `%Mob.Style{}` value under the `:style` key is merged into the node's
own props before serialisation. Inline props override style values.

## Platform blocks

Props scoped to one platform are silently ignored on the other:

    props: %{padding: 12, ios: %{padding: 20}}
    # iOS sees padding: 20; Android sees padding: 12

## Injecting a mock NIF

    Mob.Renderer.render(tree, :android, MockNIF)

# `color_props`

```elixir
@spec color_props() :: [atom()]
```

Return the set of prop keys resolved as colors (theme token or ARGB integer).

# `colors`

```elixir
@spec colors() :: %{required(atom()) =&gt; non_neg_integer()}
```

Return the full color palette map (token → ARGB integer).

# `render`

```elixir
@spec render(map(), atom(), module() | atom(), atom() | {atom(), :replace}) ::
  {:ok, :json_tree} | {:error, term()}
```

Render a component tree for the given platform.

Loads the active `Mob.Theme`, clears the tap registry, serialises the tree
to JSON, and calls `set_root/1` on the NIF. Returns `{:ok, :json_tree}`.

`transition` is an atom for the nav animation: `:push`, `:pop`, `:reset` or
`:none`, defaulting to `:none` (instant swap).

For a navigation that REPLACES the stack, the router passes
`{animation, :replace}` instead. The animation half still reaches
`set_transition/1` as a plain atom — the vocabulary native matches on stays
closed — and the replacement half is emitted as a `"replaces_stack"` key on
the JSON root, which tells native the outgoing screen is unreachable and its
retained view tree can be released.

The flag is deliberately NOT folded into the transition atom. That atom's name
reaches Android as a raw string and `MainActivity.kt` matches
`"push"`/`"pop"`/`"reset"` with an exact `when`, so any value outside that set
falls through to no animation at all. An unknown JSON key is skipped by every
parser on both platforms, so a host that does not read it keeps its behaviour.

Callers never construct the tuple; `Mob.Socket` validates the public
`:push | :pop | :reset` vocabulary.

# `text_sizes`

```elixir
@spec text_sizes() :: %{required(atom()) =&gt; float()}
```

Return the text-size scale map (token → float sp).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
