{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "fillout-flow",
  "title": "Fillout Flow",
  "description": "Schema-driven Fillout flow renderer with Circle UI and Fillout-ready submissions.",
  "registryDependencies": [
    "@blocks/fillout-flow-schema",
    "@blocks/fillout-flow-submission",
    "@blocks/use-fillout-flow",
    "@circle-ui/container",
    "@circle-ui/date-picker",
    "@circle-ui/form",
    "@circle-ui/text",
    "@circle-ui/utils"
  ],
  "files": [
    {
      "path": "registry/berlin/blocks/fillout-flow/fillout-flow.tsx",
      "content": "// Generated from packages/ui/src/components/fillout-flow/fillout-flow.tsx\n\"use client\";\n\nimport * as React from \"react\";\n\nimport { Container } from \"@/registry/berlin/circle-ui/container\";\nimport { DatePicker } from \"@/registry/berlin/circle-ui/date-picker\";\nimport { Dropdown, PhoneInput, TextArea, TextBox } from \"@/registry/berlin/circle-ui/form\";\nimport {\n  FlowActionButton,\n  FlowActions,\n  FlowHead,\n  FlowOptionButton,\n  FlowOptions,\n} from \"@/registry/berlin/blocks/flow\";\nimport { Text } from \"@/registry/berlin/circle-ui/text\";\nimport { cn } from \"@/registry/berlin/lib/utils\";\nimport { createFilloutFlowDefinition, hasFilloutValue } from \"@/registry/berlin/blocks/fillout-flow-schema\";\nimport { buildFilloutSubmissionPayload } from \"@/registry/berlin/blocks/fillout-flow-submission\";\nimport { useFilloutFlow } from \"@/registry/berlin/blocks/use-fillout-flow\";\nimport type {\n  FilloutAnswerValue,\n  FilloutFlowDefinition,\n  FilloutFlowField,\n  FilloutFlowSpec,\n  FilloutFlowStep,\n  FilloutFlowValues,\n  FilloutFormMetadata,\n  FilloutSubmissionPayload,\n  FilloutUrlParameterInput,\n} from \"@/registry/berlin/blocks/fillout-flow-types\";\n\nfunction formatDateValue(value?: Date) {\n  if (!value) {\n    return \"\";\n  }\n\n  const year = `${value.getFullYear()}`;\n  const month = `${value.getMonth() + 1}`.padStart(2, \"0\");\n  const day = `${value.getDate()}`.padStart(2, \"0\");\n\n  return `${year}-${month}-${day}`;\n}\n\nfunction parseDateValue(value: string | undefined) {\n  if (!value) {\n    return undefined;\n  }\n\n  const [year, month, day] = value.split(\"-\").map((part) => Number(part));\n  if (!year || !month || !day) {\n    return undefined;\n  }\n\n  return new Date(year, month - 1, day);\n}\n\nfunction getHeaderCopy(\n  flow: FilloutFlowDefinition,\n  step: FilloutFlowStep | null,\n) {\n  if (!step) {\n    return {\n      title: flow.title ?? flow.formName,\n      description: flow.description,\n    };\n  }\n\n  if (step.fields.length === 1) {\n    return {\n      title: step.title ?? step.fields[0].title,\n      description: step.description ?? step.fields[0].description,\n    };\n  }\n\n  return {\n    title: step.title ?? flow.title ?? flow.formName,\n    description: step.description ?? flow.description,\n  };\n}\n\nfunction getArrayValue(value: FilloutAnswerValue | undefined) {\n  if (!Array.isArray(value)) {\n    return [];\n  }\n\n  return value;\n}\n\nexport interface FilloutFlowFieldRenderProps {\n  disabled?: boolean;\n  field: FilloutFlowField;\n  setValue: (value: FilloutAnswerValue | undefined) => void;\n  value: FilloutAnswerValue | undefined;\n}\n\nexport interface FilloutFlowProps {\n  className?: string;\n  flow?: FilloutFlowDefinition;\n  form?: FilloutFormMetadata;\n  spec?: FilloutFlowSpec;\n  initialValues?: FilloutFlowValues;\n  loading?: boolean;\n  onClose?: () => void;\n  onStepChange?: (stepIndex: number, step: FilloutFlowStep) => void;\n  onSubmit?: (\n    payload: FilloutSubmissionPayload,\n    context: {\n      flow: FilloutFlowDefinition;\n      values: FilloutFlowValues;\n    },\n  ) => Promise<void> | void;\n  renderField?: (props: FilloutFlowFieldRenderProps) => React.ReactNode;\n  submitLabel?: string;\n  urlParameters?: FilloutUrlParameterInput;\n}\n\nexport function FilloutFlow({\n  className,\n  flow: providedFlow,\n  form,\n  spec,\n  initialValues,\n  loading = false,\n  onClose,\n  onStepChange,\n  onSubmit,\n  renderField,\n  submitLabel,\n  urlParameters,\n}: FilloutFlowProps) {\n  const flow = React.useMemo(() => {\n    if (providedFlow) {\n      return providedFlow;\n    }\n\n    if (form && spec) {\n      return createFilloutFlowDefinition(form, spec);\n    }\n\n    throw new Error(\n      \"FilloutFlow needs either a prebuilt flow or both a Fillout form and flow spec.\",\n    );\n  }, [form, providedFlow, spec]);\n  const flowState = useFilloutFlow({\n    flow,\n    initialValues,\n    urlParameters,\n  });\n  const [submitting, setSubmitting] = React.useState(false);\n\n  React.useEffect(() => {\n    if (flowState.currentStep) {\n      onStepChange?.(flowState.currentStepIndex, flowState.currentStep);\n    }\n  }, [flowState.currentStep, flowState.currentStepIndex, onStepChange]);\n\n  const busy = loading || submitting;\n  const currentStep = flowState.currentStep;\n  const progress =\n    flow.steps.length > 0\n      ? ((flowState.currentStepIndex + 1) / flow.steps.length) * 100\n      : 0;\n  const header = getHeaderCopy(flow, currentStep);\n\n  async function submitCurrentFlow() {\n    if (!onSubmit) {\n      return;\n    }\n\n    setSubmitting(true);\n\n    try {\n      await onSubmit(flowState.createSubmissionPayload(), {\n        flow,\n        values: flowState.values,\n      });\n    } finally {\n      setSubmitting(false);\n    }\n  }\n\n  async function continueFromCurrentStep() {\n    if (!currentStep || !flowState.canContinue || busy) {\n      return;\n    }\n\n    if (flowState.isLastStep) {\n      await submitCurrentFlow();\n      return;\n    }\n\n    flowState.goNext();\n  }\n\n  async function handleSingleChoiceAdvance(\n    field: FilloutFlowField,\n    nextValue: string,\n  ) {\n    flowState.setValue(field.id, nextValue);\n\n    if (!currentStep?.autoAdvance || busy) {\n      return;\n    }\n\n    if (flowState.isLastStep) {\n      setSubmitting(true);\n\n      try {\n        await onSubmit?.(\n          buildFilloutSubmissionPayload({\n            flow,\n            values: {\n              ...flowState.values,\n              [field.id]: nextValue,\n            },\n            urlParameters,\n          }),\n          {\n            flow,\n            values: {\n              ...flowState.values,\n              [field.id]: nextValue,\n            },\n          },\n        );\n      } finally {\n        setSubmitting(false);\n      }\n\n      return;\n    }\n\n    flowState.goNext();\n  }\n\n  function renderFieldControl(field: FilloutFlowField) {\n    const value = flowState.values[field.id];\n    const setValue = (nextValue: FilloutAnswerValue | undefined) =>\n      flowState.setValue(field.id, nextValue);\n\n    const customField = renderField?.({\n      disabled: busy,\n      field,\n      setValue,\n      value,\n    });\n\n    if (customField != null) {\n      return customField;\n    }\n\n    switch (field.kind) {\n      case \"choice\":\n        return (\n          <Dropdown\n            disabled={busy}\n            label={field.title}\n            name={field.id}\n            onChange={(event) => {\n              const nextValue = event.target.value;\n              setValue(hasFilloutValue(nextValue) ? nextValue : undefined);\n            }}\n            options={field.options.map((option) => ({\n              id: option.value,\n              title: option.label,\n            }))}\n            placeholder={field.placeholder ?? \"Select an option\"}\n            required={field.required}\n            value={typeof value === \"string\" ? value : \"\"}\n          />\n        );\n      case \"choices\": {\n        const selectedValues = getArrayValue(value);\n\n        return (\n          <div className=\"space-y-3\">\n            <div className=\"space-y-1\">\n              <Text h4>{field.title}</Text>\n              {field.description ? (\n                <Text p className=\"text-muted-foreground\">\n                  {field.description}\n                </Text>\n              ) : null}\n            </div>\n            <div className=\"space-y-3\">\n              {field.options.map((option) => {\n                const selected = selectedValues.includes(option.value);\n\n                return (\n                  <FlowOptionButton\n                    key={option.id}\n                    disabled={busy || option.disabled}\n                    info={option.info}\n                    label={option.label}\n                    onSelect={(nextSelected) => {\n                      const nextValues = nextSelected\n                        ? [...selectedValues, option.value]\n                        : selectedValues.filter((entry) => entry !== option.value);\n\n                      setValue(nextValues.length > 0 ? nextValues : undefined);\n                    }}\n                    optionType=\"option\"\n                    selected={selected}\n                    subline={option.description}\n                  />\n                );\n              })}\n            </div>\n          </div>\n        );\n      }\n      case \"date\":\n        return (\n          <div className=\"space-y-3\">\n            <div className=\"space-y-1\">\n              <Text h4>{field.title}</Text>\n              {field.description ? (\n                <Text p className=\"text-muted-foreground\">\n                  {field.description}\n                </Text>\n              ) : null}\n            </div>\n            <DatePicker\n              disabled={busy}\n              id={field.id}\n              mode=\"single\"\n              onChange={(nextValue) =>\n                setValue(\n                  nextValue ? formatDateValue(nextValue) : undefined,\n                )\n              }\n              value={typeof value === \"string\" ? parseDateValue(value) : undefined}\n            />\n          </div>\n        );\n      case \"email\":\n      case \"number\":\n      case \"text\":\n        return (\n          <div className=\"space-y-2\">\n            {field.description ? (\n              <Text p className=\"text-muted-foreground\">\n                {field.description}\n              </Text>\n            ) : null}\n            <TextBox\n              autoComplete={field.autoComplete}\n              disabled={busy}\n              label={field.title}\n              name={field.id}\n              onChange={(event) => {\n                const nextValue = event.target.value;\n                setValue(hasFilloutValue(nextValue) ? nextValue : undefined);\n              }}\n              placeholder={field.placeholder}\n              required={field.required}\n              type={\n                field.kind === \"email\"\n                  ? \"email\"\n                  : field.kind === \"number\"\n                    ? \"number\"\n                    : \"text\"\n              }\n              value={typeof value === \"string\" ? value : \"\"}\n            />\n          </div>\n        );\n      case \"phone\":\n        return (\n          <PhoneInput\n            disabled={busy}\n            label={field.title}\n            name={field.id}\n            onChange={(nextValue) =>\n              setValue(hasFilloutValue(nextValue) ? nextValue : undefined)\n            }\n            subtitle={field.description}\n            value={typeof value === \"string\" ? value : \"\"}\n          />\n        );\n      case \"textarea\":\n        return (\n          <div className=\"space-y-2\">\n            {field.description ? (\n              <Text p className=\"text-muted-foreground\">\n                {field.description}\n              </Text>\n            ) : null}\n            <TextArea\n              disabled={busy}\n              label={field.title}\n              name={field.id}\n              onChange={(event) => {\n                const nextValue = event.target.value;\n                setValue(hasFilloutValue(nextValue) ? nextValue : undefined);\n              }}\n              placeholder={field.placeholder}\n              required={field.required}\n              value={typeof value === \"string\" ? value : \"\"}\n            />\n          </div>\n        );\n      default:\n        return null;\n    }\n  }\n\n  function renderCardStep(step: FilloutFlowStep) {\n    const field = step.fields[0];\n    const selectedValue = flowState.values[field.id];\n    const selectedValues = getArrayValue(selectedValue);\n\n    return (\n      <>\n        <FlowOptions>\n          {field.options.map((option) => {\n            const selected = field.multiple\n              ? selectedValues.includes(option.value)\n              : selectedValue === option.value;\n\n            return (\n              <FlowOptionButton\n                key={option.id}\n                disabled={busy || option.disabled}\n                info={option.info}\n                label={option.label}\n                onSelect={\n                  field.multiple\n                    ? (nextSelected) => {\n                        const nextValues = nextSelected\n                          ? [...selectedValues, option.value]\n                          : selectedValues.filter(\n                              (entry) => entry !== option.value,\n                            );\n\n                        flowState.setValue(\n                          field.id,\n                          nextValues.length > 0 ? nextValues : undefined,\n                        );\n                      }\n                    : () => {\n                        flowState.setValue(field.id, option.value);\n                      }\n                }\n                onSelectAndContinue={\n                  !field.multiple && step.autoAdvance\n                    ? () => handleSingleChoiceAdvance(field, option.value)\n                    : undefined\n                }\n                optionType={field.multiple ? \"option\" : \"singleOption\"}\n                selected={selected}\n                subline={option.description}\n              />\n            );\n          })}\n        </FlowOptions>\n\n        {(!step.autoAdvance || field.multiple) && (\n          <FlowActions>\n            <FlowActionButton\n              disabled={!flowState.canContinue || busy}\n              loading={busy}\n              onClick={() => {\n                void continueFromCurrentStep();\n              }}\n            >\n              {flowState.isLastStep\n                ? submitLabel ?? step.ctaLabel ?? flow.submitLabel\n                : step.ctaLabel ?? \"Continue\"}\n            </FlowActionButton>\n          </FlowActions>\n        )}\n      </>\n    );\n  }\n\n  function renderStackStep(step: FilloutFlowStep) {\n    return (\n      <>\n        <Container maxWidth={520}>\n          <div className=\"space-y-4 px-6 pb-8 pt-2\">\n            {step.fields.map((field) => (\n              <div key={field.id}>{renderFieldControl(field)}</div>\n            ))}\n          </div>\n        </Container>\n        <FlowActions>\n          <FlowActionButton\n            disabled={!flowState.canContinue || busy}\n            loading={busy}\n            onClick={() => {\n              void continueFromCurrentStep();\n            }}\n          >\n            {flowState.isLastStep\n              ? submitLabel ?? step.ctaLabel ?? flow.submitLabel\n              : step.ctaLabel ?? \"Continue\"}\n          </FlowActionButton>\n        </FlowActions>\n      </>\n    );\n  }\n\n  if (!currentStep) {\n    return null;\n  }\n\n  const usesCardLayout =\n    currentStep.fields.length === 1 &&\n    (currentStep.fields[0].kind === \"choice\" ||\n      currentStep.fields[0].kind === \"choices\");\n\n  return (\n    <div className={cn(\"w-full\", className)}>\n      <FlowHead\n        onBackClick={() => {\n          if (!flowState.isFirstStep) {\n            flowState.goBack();\n          }\n        }}\n        onCloseClick={() => {\n          onClose?.();\n        }}\n        progress={progress}\n      >\n        {onClose ? undefined : <div className=\"size-14 shrink-0\" aria-hidden=\"true\" />}\n      </FlowHead>\n\n      <Container maxWidth={520}>\n        <div className=\"space-y-2 px-6 pb-2 pt-4\">\n          <Text h2>{header.title}</Text>\n          {header.description ? (\n            <Text p className=\"text-muted-foreground\">\n              {header.description}\n            </Text>\n          ) : null}\n        </div>\n      </Container>\n\n      {usesCardLayout ? renderCardStep(currentStep) : renderStackStep(currentStep)}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "src/components/ui/fillout/flow.tsx"
    }
  ],
  "type": "registry:ui"
}