{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "form-card",
  "title": "FormCard",
  "description": "Collect a few structured fields mid-conversation.",
  "dependencies": [
    "ai",
    "zod"
  ],
  "registryDependencies": [
    "card",
    "button",
    "input",
    "textarea",
    "shobky/fable-ui/core"
  ],
  "files": [
    {
      "path": "components/fable-ui/form-card/form-card.tsx",
      "content": "\"use client\"\n\nimport { useMemo, useState } from \"react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\"\nimport { Input } from \"@/components/ui/input\"\nimport { Textarea } from \"@/components/ui/textarea\"\nimport { cn } from \"@/lib/utils\"\n\nexport type FormCardField =\n  | {\n      name: string\n      label: string\n      type: \"text\" | \"date\" | \"textarea\"\n      required?: boolean\n      placeholder?: string\n    }\n  | {\n      name: string\n      label: string\n      type: \"number\"\n      required?: boolean\n      placeholder?: string\n      min?: number\n      max?: number\n    }\n  | {\n      name: string\n      label: string\n      type: \"select\"\n      required?: boolean\n      options: { label: string; value: string }[]\n    }\n  | { name: string; label: string; type: \"toggle\"; required?: boolean }\n\nexport type FormCardProps = {\n  title: string\n  description?: string\n  submitLabel?: string\n  fields: FormCardField[]\n  isLoading?: boolean\n  isDisabled?: boolean\n  error?: {\n    title: string\n    description?: string\n  }\n  onSubmit?: (values: Record<string, string | number | boolean>) => void\n}\n\nexport function FormCard({\n  title,\n  description,\n  submitLabel = \"Submit\",\n  fields = [],\n  isLoading,\n  isDisabled,\n  error,\n  onSubmit,\n}: FormCardProps) {\n  const initialValues = useMemo<\n    Record<string, string | number | boolean>\n  >(() => {\n    return Object.fromEntries(\n      fields.map((field) => [field.name, field.type === \"toggle\" ? false : \"\"])\n    )\n  }, [fields])\n  const [editedValues, setEditedValues] =\n    useState<Record<string, string | number | boolean>>(initialValues)\n  const values = useMemo(() => {\n    const next = { ...initialValues }\n\n    for (const field of fields) {\n      if (field.name in editedValues) {\n        next[field.name] = editedValues[field.name]\n      }\n    }\n\n    return next\n  }, [editedValues, fields, initialValues])\n  const formDisabled = Boolean(isDisabled || isLoading || error)\n\n  function setValue(name: string, value: string | number | boolean) {\n    setEditedValues((current) => ({ ...current, [name]: value }))\n  }\n\n  return (\n    <Card\n      className=\"w-full max-w-2xl\"\n      data-fable-ui=\"form-card\"\n      aria-busy={isLoading || undefined}\n    >\n      <CardHeader>\n        <CardTitle className=\"text-base\">{title || \"Collect input\"}</CardTitle>\n        {description ? <CardDescription>{description}</CardDescription> : null}\n      </CardHeader>\n      <CardContent>\n        {isLoading ? (\n          <p className=\"text-sm text-muted-foreground\">Preparing form...</p>\n        ) : null}\n        {error ? (\n          <div\n            className=\"mb-4 rounded-md border border-destructive/20 bg-destructive/5 p-3\"\n            role=\"alert\"\n          >\n            <p className=\"text-sm font-medium text-destructive\">\n              {error.title}\n            </p>\n            {error.description ? (\n              <p className=\"text-sm text-muted-foreground\">\n                {error.description}\n              </p>\n            ) : null}\n          </div>\n        ) : null}\n        <form\n          className=\"flex flex-col gap-4\"\n          onSubmit={(event) => {\n            event.preventDefault()\n            onSubmit?.(values)\n          }}\n        >\n          {fields.map((field) => {\n            const value = values[field.name]\n\n            return (\n              <label\n                key={field.name}\n                className={cn(\"flex flex-col gap-2 text-sm font-medium\", {\n                  \"flex-row-reverse items-center justify-end\":\n                    field.type === \"toggle\",\n                })}\n              >\n                <span>{field.label}</span>\n                {field.type === \"textarea\" ? (\n                  <Textarea\n                    value={String(value ?? \"\")}\n                    placeholder={field.placeholder}\n                    required={field.required}\n                    disabled={formDisabled}\n                    onChange={(event) =>\n                      setValue(field.name, event.target.value)\n                    }\n                  />\n                ) : field.type === \"select\" ? (\n                  <select\n                    className=\"h-9 rounded-md border bg-background px-3 text-sm\"\n                    value={String(value ?? \"\")}\n                    required={field.required}\n                    disabled={formDisabled}\n                    onChange={(event) =>\n                      setValue(field.name, event.target.value)\n                    }\n                  >\n                    <option value=\"\">Select...</option>\n                    {field.options.map((option) => (\n                      <option key={option.value} value={option.value}>\n                        {option.label}\n                      </option>\n                    ))}\n                  </select>\n                ) : field.type === \"toggle\" ? (\n                  <input\n                    type=\"checkbox\"\n                    checked={Boolean(value)}\n                    disabled={formDisabled}\n                    onChange={(event) =>\n                      setValue(field.name, event.target.checked)\n                    }\n                  />\n                ) : (\n                  <Input\n                    type={field.type}\n                    value={String(value ?? \"\")}\n                    min={field.type === \"number\" ? field.min : undefined}\n                    max={field.type === \"number\" ? field.max : undefined}\n                    placeholder={field.placeholder}\n                    required={field.required}\n                    disabled={formDisabled}\n                    onChange={(event) =>\n                      setValue(\n                        field.name,\n                        field.type === \"number\" && event.target.value !== \"\"\n                          ? Number(event.target.value)\n                          : event.target.value\n                      )\n                    }\n                  />\n                )}\n              </label>\n            )\n          })}\n          <CardFooter className=\"px-0 pb-0\">\n            <Button\n              type=\"submit\"\n              disabled={formDisabled || fields.length === 0}\n            >\n              {submitLabel}\n            </Button>\n          </CardFooter>\n        </form>\n      </CardContent>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/fable-ui/form-card/form-card.tsx"
    },
    {
      "path": "components/fable-ui/form-card/index.ts",
      "content": "export * from \"./form-card\"\n",
      "type": "registry:component",
      "target": "@components/fable-ui/form-card/index.ts"
    },
    {
      "path": "lib/fable-ui/tools/collect-input-tool.ts",
      "content": "import { tool } from \"ai\"\nimport { z } from \"zod\"\n\nimport { defineFableComponent } from \"@/lib/fable-ui/core/definitions\"\nimport { FormCard } from \"@/components/fable-ui/form-card/form-card\"\n\nconst baseField = z.object({\n  name: z.string().min(1),\n  label: z.string().min(1),\n  required: z.boolean().optional(),\n  placeholder: z.string().optional(),\n})\n\nconst looseField = z\n  .object({\n    name: z.string().optional(),\n    label: z.string().optional(),\n    type: z.string().optional(),\n    required: z.boolean().optional(),\n    placeholder: z.string().optional(),\n    min: z.number().optional(),\n    max: z.number().optional(),\n    options: z\n      .array(\n        z\n          .object({\n            label: z.string().optional(),\n            value: z.union([z.string(), z.number(), z.boolean()]).optional(),\n          })\n          .passthrough()\n      )\n      .optional(),\n  })\n  .passthrough()\n\nexport const collectInputSchema = z.object({\n  title: z.string().min(1),\n  description: z.string().optional(),\n  submitLabel: z.string().min(1).optional(),\n  fields: z\n    .array(\n      z.discriminatedUnion(\"type\", [\n        baseField.extend({ type: z.literal(\"text\") }),\n        baseField.extend({ type: z.literal(\"date\") }),\n        baseField.extend({ type: z.literal(\"textarea\") }),\n        baseField.extend({\n          type: z.literal(\"number\"),\n          min: z.number().optional(),\n          max: z.number().optional(),\n        }),\n        baseField.extend({\n          type: z.literal(\"select\"),\n          options: z\n            .array(\n              z.object({ label: z.string().min(1), value: z.string().min(1) })\n            )\n            .min(1)\n            .max(12),\n        }),\n        baseField.extend({ type: z.literal(\"toggle\") }),\n      ])\n    )\n    .min(1)\n    .max(8),\n})\n\nexport const collectInputToolSchema = z\n  .object({\n    title: z\n      .string()\n      .optional()\n      .describe(\"Short title for the form. Required for valid rendering.\"),\n    description: z.string().optional(),\n    submitLabel: z.string().optional(),\n    fields: z\n      .array(looseField)\n      .max(8)\n      .optional()\n      .describe(\n        \"One to eight fields. Valid field types are text, number, select, date, textarea, and toggle. Select fields require non-empty string options.\"\n      ),\n  })\n  .passthrough()\n\nexport type CollectInput = z.infer<typeof collectInputSchema>\nexport type CollectInputToolInput = z.infer<typeof collectInputToolSchema>\n\nexport function createCollectInputTool() {\n  return tool({\n    description:\n      \"Collect a few structured fields mid-conversation. Keep forms short; valid payloads need title and fields. The host must validate submitted values.\",\n    inputSchema: collectInputToolSchema,\n  })\n}\n\nexport const collectInput = defineFableComponent({\n  name: \"collect_input\",\n  schema: collectInputSchema,\n  tool: createCollectInputTool(),\n  renderer: {\n    Component: FormCard,\n    loadingProps: { title: \"Collect input\", fields: [], isLoading: true },\n    emptyProps: { title: \"Collect input\", fields: [] },\n    errorProps: (description: string) => ({\n      title: \"Form unavailable\",\n      fields: [],\n      error: { title: \"Form unavailable\", description },\n    }),\n    toProps: (data: CollectInput, handlers) => ({\n      ...data,\n      onSubmit: handlers.onFormSubmit,\n    }),\n  },\n})\n",
      "type": "registry:lib",
      "target": "@lib/fable-ui/tools/collect-input-tool.ts"
    },
    {
      "path": "lib/fable-ui/manifests/collect-input.md",
      "content": "---\ntool: collect_input\ntype: registry:component\n---\n\n# collect_input\n\nUse `collect_input` for a short, structured mid-conversation form.\n\nValid payloads need a title and one to eight fields. Supported field types are `text`, `number`, `select`, `date`, `textarea`, and `toggle`; `select` fields need non-empty string options.\n\nKeep v1 forms simple. The host app must validate submitted values server-side and decide what to do with them.\n",
      "type": "registry:file",
      "target": "@lib/fable-ui/manifests/collect-input.md"
    }
  ],
  "type": "registry:component"
}