# `form_input`

## Use when / do not use when

Use for non-choice structured input. The reserved email-capture screen also uses this type. Do not use for option lists or payment-entry forms.

## Exact metadata contract

<!-- funnelsgrove:generated:start contract-v3/step/form_input -->
### Generated contract facts — do not edit

- Step contract version: `3`
- Contract hash: `d761e91d5ac6ff9e72c6d49c5bcd014270912473a66f99c49d726c65998085cf`
- Type: `form_input`
- Family: `form`
- Description: Non-choice structured input, including reserved email capture.
- Action owner: `either`
- Analytics role: _none_
- Terminal: `false`
- Required kind: _none_
- Allowed kinds: _None_
- Allowed semantic events: `email_captured`
- Forbidden capabilities: `choice-write`, `checkout`, `subscription-management`, `subscription-handoff`, `hardcoded-navigation`, `provider-direct-analytics`, `browser-registration-completed`

#### Choice contract

_Not a choice type._

#### Automatic lifecycle events

`first_step_clicked`, `first_step_viewed`, `funnel_started`, `step_completed`, `step_exited`, `step_started`

#### Approved capability owners

- `email-capture` must be owned by `submitEmailCapture`.

#### Reserved identities

- `email-capture`: id=`email-capture`, name=`email-capture`; type=`form_input`; unique=`true`

#### Golden fixture source

```ts
import type { FunnelManifestStep, FunnelStepMeta } from '@funnelsgrove/runtime';

const manifestStep = {
  id: 'email-capture',
  name: 'email-capture',
  path: '/email-capture',
  filePath: 'src/steps/step-email-capture.tsx',
  componentKey: 'stepEmailCapture',
  type: 'form_input',
  title: 'Your email',
} as const satisfies FunnelManifestStep;

const componentMeta = {
  id: 'email-capture',
  name: 'email-capture',
  type: 'form_input',
  figmaNodeId: 'golden:form-input',
  title: 'Your email',
  description: 'Capture an email through the transactional helper.',
} as const satisfies FunnelStepMeta;

export const goldenFixture = {
  stepType: 'form_input',
  manifestStep,
  componentMeta,
  helper: 'submitEmailCapture',
  forbiddenCalls: ['trackLead', 'provider APIs', 'hard-coded paywall navigation'],
} as const;
```
<!-- funnelsgrove:generated:end contract-v3/step/form_input -->

## Interaction and persisted answer shape

Validate local input before submission. For the reserved email screen, pass the email to `submitEmailCapture`; the helper owns persistence, event creation, lifecycle completion, and routing.

## Automatic lifecycle events

The controller owns lifecycle events. A failed email request leaves state, lifecycle, and navigation unchanged. Preview makes no API or provider call.

## Required semantic conversions

The reserved email screen produces `email_captured` transactionally on the server. Only a newly created conversion may fan out browser Pixel/Google events with the returned event ID.

## Approved shared helpers

Use `submitEmailCapture`. Use the manifest flow controller for the next route.

## MUST rules

- Keep the reserved email identity exact.
- Disable duplicate submits while one request is in flight.
- Complete only after the API succeeds.

## NEVER rules

- Never call `trackLead`, PostHog, Meta CAPI, Pixel, Google, or any provider directly.
- Never generate a browser event ID or hard-code paywall navigation.
- Never put raw email or other PII in analytics event metadata or logs.
- Only the server delivery boundary may use raw email as Meta hashing input.
- PostHog properties must remain non-PII.

## Golden template or fixture

See the [embedded canonical fixture](#golden-fixture-source) generated from `form_input.fixture.ts`.

## Validation command

Run `fgrove validate`.

## Manual QA and completion criteria

Test success, duplicate, API failure, double-submit, preview, and stale-visit cases; verify only a newly created conversion triggers browser fanout.
