# NativeDynamic

Convert callback values and variant payloads to native styles with compiled binding rules.

Native compilation rewrites a callback [`style`](/docs/api/core/style), or a [`variants`](/docs/api/core/variants) definition with callback choices, into a `NativeDynamic.create` call. Applications normally do not import it. A binding program replaces the authored callback, which never runs on the device.

```ts title="meter.ts"
import { NativeDynamic } from 'zyzz/runtime'

const meter = NativeDynamic.create({
  axes: {},
  defaults: {},
  // Assigns the `width` field to the width property
  program: {
    rules: [
      {
        matches: [],
        steps: [{ parts: [{ slot: '--meter-width' }], property: 'width' }],
      },
    ],
    slots: { width: '--meter-width' },
  },
  styles: {},
  // The cast mirrors generated code
}) as NativeDynamic.Callable<{ width: string }>

// { style: { width: 12 } }
export const props = meter({ width: '12px' })
```

## Signature

```ts
// Generated program and fragments for one set and scheme
NativeDynamic.create(options)
```

## Parameters

### options.axes

* **Type:** `{ [axis: string]: readonly string[] }`

Each axis and all of its choices, static and dynamic, as in [`NativeStatic.create`](/docs/api/runtime/namespaces/NativeStatic#optionsaxes).

```ts
// `custom` takes a payload and `fixed` does not
NativeDynamic.create({
  axes: { size: ['custom', 'fixed'] },
  defaults: {},
  payloads,
  program,
  styles,
})
```

### options.conditions

* **Type:** `readonly string[]`
* **Default:** `undefined`

Accepted through `Recipe.Definition` but ignored. Native compilation resolves conditions before this helper runs, so conditional slot records in `payloads` are never read.

```ts
// Same result as without `conditions`
NativeDynamic.create({ axes, conditions: ['wide'], defaults, program, styles })
```

### options.defaults

* **Type:** `{ [axis: string]: string | null }`

The choice used when an axis is omitted or `undefined`. `null` clears the axis.

```ts
// An omitted size selects fixed
NativeDynamic.create({
  axes,
  defaults: { size: 'fixed' },
  payloads,
  program,
  styles,
})
```

### options.defaultPayloads

* **Type:** `{ [axis: string]: { [field: string]: string | number } }`
* **Default:** `undefined`

Field values for an axis whose default is a dynamic choice.

```ts
// An omitted size selects custom with 4px padding
NativeDynamic.create({
  axes,
  defaults: { size: 'custom' },
  defaultPayloads: { size: { padding: '4px' } },
  payloads,
  program,
  styles,
})
```

### options.payloads

* **Type:** `readonly Recipe.Payload[]`
* **Default:** `undefined`

The dynamic choices, as in [`Recipe.Payload`](/docs/api/runtime/namespaces/Recipe#types). Native output uses only the base slots.

```ts
// The `padding` field of `custom` binds `--box-padding`
const payloads = [
  { axis: 'size', choice: 'custom', slots: [{ padding: '--box-padding' }] },
]
```

### options.program

* **Type:** `NativeDynamic.Program`

`slots` maps required top-level fields to slot names. `rules` lists base, variant, and compound rules in authored order, as in [`NativeStatic.create`](/docs/api/runtime/namespaces/NativeStatic#optionsrules). A step either names a fragment or joins `parts`, literals and slot values, into one property value.

The compiler marks a numeric field's slot part with `number: true` and omits the marker otherwise. A marked part requires a number, and a string value throws `Native.SelectionError`.

```ts
// The fixed choice applies a fragment, and custom binds its padding
const program = {
  rules: [
    {
      matches: [['size', ['custom']]],
      steps: [{ parts: [{ slot: '--box-padding' }], property: 'padding' }],
    },
    { matches: [['size', ['fixed']]], steps: [{ style: 'fixed' }] },
  ],
  slots: {},
}
```

```ts
// Binds 0.5 for opacity and rejects '0.5'
const parts = [{ number: true, slot: '--meter-opacity' }] as const
```

### options.styles

* **Type:** `{ [name: string]: StyleSheet.NativeStyle }`

Converted native fragments named by `style` steps. They are frozen at creation.

```ts
// The fragment for the fixed choice
NativeDynamic.create({
  axes,
  defaults,
  payloads,
  program,
  styles: { fixed: { padding: 0 } },
})
```

### options.units

* **Type:** `{ px?: number; rem?: number }`
* **Default:** `undefined`

Logical-unit scales for converting bound lengths. `px` multiplies bound `px` lengths and defaults to `1`. `rem` has no default, so a bound `rem` value throws without it.

```ts
// '1rem' binds as 16
NativeDynamic.create({ axes, defaults, program, styles, units: { rem: 16 } })
```

```ts
// '4px' binds as 8
NativeDynamic.create({ axes, defaults, program, styles, units: { px: 2 } })
```

### options.fonts

* **Type:** `{ [family: string]: string }`
* **Default:** `undefined`

Authored font family names mapped to native font names for bound `fontFamily` values.

```ts
// A bound 'Geist' becomes 'Geist-Regular'
NativeDynamic.create({
  axes,
  defaults,
  fonts: { Geist: 'Geist-Regular' },
  program,
  styles,
})
```

## Application

### input\[field]

* **Type:** `string | number`

A required callback field. Generated code casts the callable to `NativeDynamic.Callable<input>` with the authored fields, while the helper's own return type accepts any record. Bound values convert with the scalar rules of static native compilation, except `flex`.

Shorthands expand to longhands. `padding`, `margin`, and `inset` assign each side, `borderColor`, `borderWidth`, and `borderRadius` assign every side or corner, and `gap` assigns `columnGap` and `rowGap`. Later steps overwrite earlier longhands.

`flex` is the exception. A bound `flex` value is validated, but the step always assigns `flex: 0` instead of the `flexGrow`, `flexShrink`, and `flexBasis` that static compilation emits.

```ts
// { style: { width: '50%' } }
meter({ width: '50%' })
```

### input\[axis]

* **Type:** `string | boolean | { [choice: string]: { [field: string]: string | number } } | null | undefined`

A static choice name, or a dynamic choice with its field values.

```ts
// Expands to paddingTop, paddingRight, paddingBottom, and paddingLeft
box({ size: { custom: { padding: '12px' } } })
```

### input.style

* **Type:** `StyleSheet.StyleProp<overrides>`
* **Default:** `undefined`

Native overrides placed after the generated style, with their identity and type kept. Host objects, such as `Animated` values, enter only here, never as fields. A falsy override, such as `false` or `null`, is dropped.

```ts
// { style: [{ width: 12 }, { opacity: 0.5 }] }
meter({ width: '12px', style: { opacity: 0.5 } })
```

## Returns

### props

* **Type:** `Native.Props<style>`

`{ style }`, with the generated style alone or followed by the override. Each call returns new props. A style with bound values is also new on every call. Without fields or payloads, the generated style itself is frozen and reused for up to 256 combinations.

```ts
// { width: 12 }
props.style
```

### style

* **Type:** `StyleSheet.StyleProp<style>`

The generated style, or a new `[generated, override]` array when `input.style` is truthy.

```ts
// [{ width: 12 }, { opacity: 0.5 }]
meter({ style: { opacity: 0.5 }, width: '12px' }).style
```

## Types

* **`NativeDynamic.Callable<input>`:** A callable with required input fields.
* **`NativeDynamic.create.Options`:** The full input, including `units` and `fonts`.
* **`NativeDynamic.From<fn>`:** The native callable for a published web callable, with native `style` overrides in place of `className`, `style`, and `vars`.
* **`NativeDynamic.Program`:** The `slots` and ordered `rules`.
* **`NativeDynamic.RecipeCallable<input>`:** A variant callable whose input may be omitted.

```ts title="meter.ts"
import type { NativeDynamic } from 'zyzz/runtime'

// A native callable that requires `width` on each call
export type Meter = NativeDynamic.Callable<{ width: string }>
```

## Errors

The callable throws [`Native.SelectionError`](/docs/api/runtime/namespaces/Native#nativeselectionerror) before returning for an unknown input key or choice, a missing, non-scalar, or non-finite field, a payload on a static choice, a payload object without exactly one choice, or a rule that references a missing fragment or unbound slot.

A dynamic choice declared without a base slot record throws a `TypeError`. A value that cannot convert throws an `Error`, such as a `rem` length without `units.rem`.

A numeric `lineHeight` from the program is a multiplier. It throws `Native.SelectionError` when it is negative or not finite, when the merged style has no numeric `fontSize`, or when the product is not finite.

```ts title="invalid.ts"
import { Native, NativeDynamic } from 'zyzz/runtime'

const meter = NativeDynamic.create({
  axes: {},
  defaults: {},
  program: { rules: [], slots: { width: '--meter-width' } },
  styles: {},
})

try {
  // Every program slot is required
  meter({} as never)
} catch (error) {
  if (error instanceof Native.SelectionError) console.error(error.message)
}
```
