{
  "schemaVersion": 1,
  "name": "form",
  "useFor": "Build a validated form with react-hook-form: wrap it in Form with the useForm result, give each field a FormField with a render function returning FormItem, FormLabel, FormControl around the one control, then FormDescription and FormMessage. FormControl passes the id, the described-by ids and aria-invalid to the control, so the red edge and the message appear together. Finish with one full-width default Button of type submit, showing a Spinner while it works.",
  "constraints": [
    "A red label on an invalid field",
    "Validating on every keystroke before the first submit",
    "A submit button that stays enabled while the request runs",
    "More than one purple pill in the form"
  ],
  "documentation": "",
  "source": "Figma 427:2715: Create Agent Identity (handle check states, Creating Agent Identity...), Login",
  "exports": [
    "Form",
    "FormControl",
    "FormDescription",
    "FormField",
    "FormItem",
    "FormLabel",
    "FormMessage",
    "useFormField"
  ],
  "client": true,
  "docsUrl": "https://inkbox-ds.vercel.app/components/form",
  "markdownUrl": "https://inkbox-ds.vercel.app/r/form.md",
  "examples": [
    {
      "path": "examples/form.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { AppleLogoIcon, ArrowsLeftRightIcon, CheckCircleIcon, EnvelopeSimpleIcon, GlobeIcon, InfoIcon, PhoneIcon, PlusIcon, XCircleIcon } from \"@phosphor-icons/react/ssr\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport { InputGroup, InputGroupAddon, InputGroupInput } from \"@/components/ui/input-group\";\nimport { Label } from \"@/components/ui/label\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\nimport { Spinner } from \"@/components/ui/spinner\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { Textarea } from \"@/components/ui/textarea\";\n\ntype Values = { handle: string; displayName: string; description: string; phone: boolean; imessage: boolean; a2a: boolean; tls: \"edge\" | \"passthrough\" };\n\nconst TAKEN = [\"acme\", \"support\", \"acme-support\"];\nconst wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\n// The availability check a real form would make against the API: here, a short wait and a fixed list.\nasync function isAvailable(handle: string) {\n  await wait(600);\n  return !TAKEN.includes(handle);\n}\n\nconst CHANNELS = [\n  { name: \"phone\", label: \"Phone Number\", icon: PhoneIcon },\n  { name: \"imessage\", label: \"iMessage\", icon: AppleLogoIcon },\n] as const;\n\nexport function Preview() {\n  const form = useForm<Values>({ mode: \"onSubmit\", reValidateMode: \"onChange\", defaultValues: { handle: \"\", displayName: \"\", description: \"\", phone: false, imessage: false, a2a: false, tls: \"edge\" } });\n  const [check, setCheck] = React.useState<\"idle\" | \"checking\" | \"free\" | \"taken\">(\"idle\");\n  const [created, setCreated] = React.useState<string | null>(null);\n  const handle = form.watch(\"handle\");\n\n  // Check the handle a moment after typing stops; the result shows as the field's trailing glyph.\n  React.useEffect(() => {\n    if (!/^[a-z0-9-]{3,}$/.test(handle)) return setCheck(\"idle\");\n    let live = true;\n    setCheck(\"checking\");\n    const t = setTimeout(() => isAvailable(handle).then((ok) => live && setCheck(ok ? \"free\" : \"taken\")), 300);\n    return () => {\n      live = false;\n      clearTimeout(t);\n    };\n  }, [handle]);\n\n  async function onSubmit(values: Values) {\n    if (!(await isAvailable(values.handle))) {\n      form.setError(\"handle\", { message: \"(is not available)\" });\n      return;\n    }\n    await wait(1200);\n    setCreated(values.handle);\n  }\n\n  const submitting = form.formState.isSubmitting;\n  if (created)\n    return (\n      <div className=\"mx-auto grid w-full max-w-121.5 justify-items-center gap-3 py-6 text-center\">\n        <CheckCircleIcon weight=\"fill\" className=\"size-10 text-success\" />\n        <p className=\"text-h3 leading-heading text-fg\">@{created} is ready!</p>\n        <Button variant=\"secondary\" onClick={() => { form.reset(); setCreated(null); }}>\n          Create another\n        </Button>\n      </div>\n    );\n\n  return (\n    <Form {...form}>\n      <form onSubmit={form.handleSubmit(onSubmit)} noValidate className=\"mx-auto grid w-full max-w-121.5 gap-6\">\n        <fieldset disabled={submitting} className=\"m-0 grid gap-6 border-0 p-0 disabled:opacity-60\">\n          <div className=\"flex gap-5\">\n            <button type=\"button\" aria-label=\"Add a picture\" className=\"flex size-16 shrink-0 items-center justify-center rounded-pill bg-subtle text-fg outline-none transition-[background-color,scale] duration-150 ease-out hover:bg-subtle-hover active:scale-97 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring motion-reduce:transition-none\">\n              <PlusIcon className=\"size-7\" />\n            </button>\n            <div className=\"grid min-w-0 flex-1 gap-3\">\n              <FormField\n                control={form.control}\n                name=\"handle\"\n                rules={{\n                  required: \"Choose a handle for the agent.\",\n                  pattern: { value: /^[a-z0-9-]{3,}$/, message: \"Use at least 3 lowercase letters, numbers or hyphens.\" },\n                  validate: () => check !== \"taken\" || \"(is not available)\",\n                }}\n                render={({ field, fieldState }) => (\n                  <FormItem>\n                    <FormLabel className=\"sr-only\">Agent handle</FormLabel>\n                    <InputGroup>\n                      <FormControl>\n                        <InputGroupInput placeholder=\"Agent handle\" autoComplete=\"off\" spellCheck={false} {...field} aria-invalid={!!fieldState.error || check === \"taken\"} />\n                      </FormControl>\n                      <InputGroupAddon align=\"inline-end\">\n                        {check === \"checking\" && <Spinner aria-label=\"Checking the handle\" className=\"text-fg-faint\" />}\n                        {check === \"free\" && <CheckCircleIcon weight=\"fill\" className=\"text-success\" aria-label=\"Available\" />}\n                        {check === \"taken\" && <XCircleIcon weight=\"fill\" className=\"text-danger-strong\" aria-label=\"Not available\" />}\n                      </InputGroupAddon>\n                    </InputGroup>\n                    {handle && check !== \"idle\" && check !== \"checking\" ? (\n                      <p className=\"flex flex-wrap items-center gap-2 text-caption leading-body text-fg-secondary\">\n                        Will be displayed as <span className=\"rounded-sm bg-subtle px-2 py-0.5 text-fg\">@{handle}</span>\n                        {check === \"taken\" && <span className=\"text-danger\">(is not available)</span>}\n                      </p>\n                    ) : null}\n                    {check !== \"taken\" && <FormMessage />}\n                  </FormItem>\n                )}\n              />\n              <FormField\n                control={form.control}\n                name=\"displayName\"\n                rules={{ maxLength: { value: 40, message: \"Keep the display name under 40 characters.\" } }}\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel className=\"sr-only\">Agent display name</FormLabel>\n                    <FormControl>\n                      <Input placeholder=\"Agent display name\" {...field} />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n              <FormField\n                control={form.control}\n                name=\"description\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel className=\"sr-only\">Agent description</FormLabel>\n                    <FormControl>\n                      <Textarea placeholder=\"Agent Description\" {...field} />\n                    </FormControl>\n                  </FormItem>\n                )}\n              />\n            </div>\n          </div>\n\n          <div className=\"grid gap-2.5\">\n            <p className=\"text-body leading-body text-fg\">Channels</p>\n            <div className=\"grid gap-3 rounded-control bg-subtle p-3.5\">\n              <div className=\"flex items-center gap-2.5\">\n                <span className=\"flex size-7 items-center justify-center rounded-sm bg-pink text-rose\">\n                  <EnvelopeSimpleIcon weight=\"fill\" className=\"size-5.5\" />\n                </span>\n                <span className=\"flex-1 text-body-lg font-medium text-fg\">Email</span>\n                <Badge variant=\"success\" size=\"lg\">\n                  <CheckCircleIcon weight=\"fill\" />\n                  Included\n                </Badge>\n              </div>\n              <InputGroup>\n                <InputGroupInput aria-label=\"Mailbox\" value={handle || \"acme-support\"} readOnly />\n                <InputGroupAddon align=\"inline-end\" variant=\"segment\">\n                  @inboxmail.com\n                </InputGroupAddon>\n              </InputGroup>\n            </div>\n            {CHANNELS.map(({ name, label, icon: Icon }) => (\n              <FormField\n                key={name}\n                control={form.control}\n                name={name}\n                render={({ field }) => (\n                  <FormItem className=\"flex h-13 items-center gap-2.5 rounded-control bg-subtle px-3.5\">\n                    <span className=\"flex size-7 items-center justify-center rounded-sm bg-pink text-rose\">\n                      <Icon weight=\"fill\" className=\"size-5.5\" />\n                    </span>\n                    <FormLabel className=\"flex-1 text-body-lg font-medium\">{label}</FormLabel>\n                    <FormControl>\n                      <Switch checked={field.value} onCheckedChange={field.onChange} onBlur={field.onBlur} />\n                    </FormControl>\n                  </FormItem>\n                )}\n              />\n            ))}\n            <FormField\n              control={form.control}\n              name=\"a2a\"\n              render={({ field }) => (\n                <FormItem className=\"flex h-13 items-center gap-2.5 rounded-control bg-subtle px-3.5\">\n                  <ArrowsLeftRightIcon className=\"size-6 text-icon-secondary\" aria-hidden />\n                  <FormLabel className=\"flex-1 text-body-lg font-medium\">\n                    Agent2Agent <Badge variant=\"pink\">BETA</Badge>\n                  </FormLabel>\n                  <FormControl>\n                    <Switch checked={field.value} onCheckedChange={field.onChange} onBlur={field.onBlur} />\n                  </FormControl>\n                </FormItem>\n              )}\n            />\n          </div>\n\n          <div className=\"grid gap-2\">\n            <p className=\"text-body leading-body text-fg\">Your agent will be reachable at:</p>\n            <p className=\"flex h-10 items-center gap-2.5 rounded-control bg-subtle px-2.5 text-body text-fg\">\n              <span className=\"flex size-7 items-center justify-center rounded-sm bg-pink text-rose\">\n                <GlobeIcon weight=\"fill\" className=\"size-5.5\" />\n              </span>\n              {handle || \"acme-support\"}.inkboxwire.com\n            </p>\n          </div>\n\n          <FormField\n            control={form.control}\n            name=\"tls\"\n            render={({ field }) => (\n              <FormItem className=\"gap-2\">\n                <FormLabel className=\"gap-1.5\">\n                  Tunnel TLS mode <InfoIcon weight=\"fill\" aria-hidden className=\"size-4.5 text-fg-label\" />\n                </FormLabel>\n                <FormControl>\n                  <RadioGroup value={field.value} onValueChange={field.onChange}>\n                    <div className=\"flex items-center gap-2\">\n                      <RadioGroupItem value=\"edge\" id=\"tls-edge\" />\n                      <Label htmlFor=\"tls-edge\">Edge (default)</Label>\n                    </div>\n                    <div className=\"flex items-center gap-2\">\n                      <RadioGroupItem value=\"passthrough\" id=\"tls-passthrough\" />\n                      <Label htmlFor=\"tls-passthrough\">Passthrough</Label>\n                    </div>\n                  </RadioGroup>\n                </FormControl>\n                <FormDescription>Edge ends TLS at Inkbox; passthrough sends it to your agent untouched.</FormDescription>\n              </FormItem>\n            )}\n          />\n        </fieldset>\n\n        <Button type=\"submit\" className=\"w-full\" disabled={submitting || check === \"checking\"}>\n          {submitting ? (\n            <>\n              <Spinner aria-hidden className=\"size-5\" />\n              Creating Agent Identity...\n            </>\n          ) : (\n            \"Create Agent Identity\"\n          )}\n        </Button>\n      </form>\n    </Form>\n  );\n}\n\nexport function Login() {\n  const form = useForm<{ email: string; password: string }>({ defaultValues: { email: \"\", password: \"\" } });\n  const [done, setDone] = React.useState(false);\n  return (\n    <Form {...form}>\n      <form onSubmit={form.handleSubmit(async () => { await wait(800); setDone(true); })} noValidate className=\"mx-auto grid w-full max-w-86 gap-3\">\n        <FormField\n          control={form.control}\n          name=\"email\"\n          rules={{ required: \"Enter your email address.\", pattern: { value: /^\\S+@\\S+\\.\\S+$/, message: \"Enter a valid email address.\" } }}\n          render={({ field }) => (\n            <FormItem>\n              <FormLabel>Email address</FormLabel>\n              <FormControl>\n                <Input type=\"email\" placeholder=\"your@email.com\" autoComplete=\"email\" {...field} />\n              </FormControl>\n              <FormMessage />\n            </FormItem>\n          )}\n        />\n        <FormField\n          control={form.control}\n          name=\"password\"\n          rules={{ required: \"Enter your password.\", minLength: { value: 8, message: \"Passwords have at least 8 characters.\" } }}\n          render={({ field }) => (\n            <FormItem>\n              <div className=\"flex items-center justify-between gap-3\">\n                <FormLabel>Password</FormLabel>\n                <a href=\"#forgot\" className=\"text-body leading-body text-fg-brand underline-offset-4 hover:underline\">Forgot password?</a>\n              </div>\n              <FormControl>\n                <Input type=\"password\" autoComplete=\"current-password\" {...field} />\n              </FormControl>\n              <FormMessage />\n            </FormItem>\n          )}\n        />\n        <Button type=\"submit\" className=\"mt-1 w-full\" disabled={form.formState.isSubmitting}>\n          {form.formState.isSubmitting ? <Spinner aria-hidden className=\"size-5\" /> : null}\n          {done ? \"Logged in\" : \"Log in\"}\n        </Button>\n      </form>\n    </Form>\n  );\n}\n",
      "exports": [
        "Preview",
        "Login"
      ],
      "requires": [
        "form",
        "badge",
        "button",
        "input",
        "input-group",
        "label",
        "radio-group",
        "spinner",
        "switch",
        "textarea"
      ],
      "packages": [
        "react-hook-form",
        "@phosphor-icons/react"
      ],
      "previews": [
        {
          "name": "Preview",
          "url": "https://inkbox-ds.vercel.app/preview/form/Preview"
        },
        {
          "name": "Login",
          "url": "https://inkbox-ds.vercel.app/preview/form/Login"
        }
      ]
    }
  ],
  "type": "ui",
  "title": "Form",
  "description": "react-hook-form fields laid out as the console's forms: a 14px label, the control, a muted description and a 12px red message.",
  "surface": "site",
  "kinds": [],
  "requires": [
    "theme",
    "label",
    "utils"
  ],
  "packages": [
    "radix-ui",
    "react-hook-form"
  ],
  "files": [
    {
      "path": "components/ui/form.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Slot } from \"radix-ui\";\nimport { Controller, FormProvider, useFormContext, useFormState, type ControllerProps, type FieldPath, type FieldValues } from \"react-hook-form\";\n\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\n\n// A react-hook-form form laid out as the console's (Figma 427:2715, Create Agent Identity, Login, Vault). Each FormItem\n// is a field: its 14px regular ink label on a 24px line, 4px above the control; fields sit 12px apart in the form. A\n// FormDescription is 14px muted under the control; a FormMessage is the 12px red line (\"(is not available)\"). A field\n// that fails keeps its label ink: only the control (aria-invalid, the red edge) and the message turn red.\nconst Form = FormProvider;\n\ntype FormFieldContextValue<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>> = { name: TName };\n\nconst FormFieldContext = React.createContext<FormFieldContextValue | null>(null);\n\nfunction FormField<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>>(props: ControllerProps<TFieldValues, TName>) {\n  return (\n    <FormFieldContext.Provider value={{ name: props.name }}>\n      <Controller {...props} />\n    </FormFieldContext.Provider>\n  );\n}\n\ntype FormItemContextValue = { id: string };\n\nconst FormItemContext = React.createContext<FormItemContextValue | null>(null);\n\n/** The state of the field around it: its ids for labels and messages, and react-hook-form's error, dirty and touched state. */\nfunction useFormField() {\n  const fieldContext = React.useContext(FormFieldContext);\n  const itemContext = React.useContext(FormItemContext);\n  const { getFieldState } = useFormContext();\n  if (!fieldContext) throw new Error(\"useFormField should be used within <FormField>\");\n  if (!itemContext) throw new Error(\"useFormField should be used within <FormItem>\");\n  const formState = useFormState({ name: fieldContext.name });\n  const fieldState = getFieldState(fieldContext.name, formState);\n  const { id } = itemContext;\n  return { id, name: fieldContext.name, formItemId: `${id}-form-item`, formDescriptionId: `${id}-form-item-description`, formMessageId: `${id}-form-item-message`, ...fieldState };\n}\n\nfunction FormItem({ className, ...props }: React.ComponentProps<\"div\">) {\n  const id = React.useId();\n  return (\n    <FormItemContext.Provider value={{ id }}>\n      <div data-slot=\"form-item\" className={cn(\"grid min-w-0 gap-1\", className)} {...props} />\n    </FormItemContext.Provider>\n  );\n}\n\nfunction FormLabel({ className, ...props }: React.ComponentProps<typeof Label>) {\n  const { error, formItemId } = useFormField();\n  return <Label data-slot=\"form-label\" data-error={!!error} htmlFor={formItemId} className={cn(\"w-fit\", className)} {...props} />;\n}\n\n/** Hands the field's id, its description and message ids and aria-invalid to the one control inside it. */\nfunction FormControl(props: React.ComponentProps<typeof Slot.Root>) {\n  const { error, formItemId, formDescriptionId, formMessageId } = useFormField();\n  return <Slot.Root data-slot=\"form-control\" id={formItemId} aria-describedby={!error ? formDescriptionId : `${formDescriptionId} ${formMessageId}`} aria-invalid={!!error} {...props} />;\n}\n\nfunction FormDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n  const { formDescriptionId } = useFormField();\n  return <p data-slot=\"form-description\" id={formDescriptionId} className={cn(\"m-0 font-sans text-body leading-body text-fg-muted\", className)} {...props} />;\n}\n\nfunction FormMessage({ className, children, ...props }: React.ComponentProps<\"p\">) {\n  const { error, formMessageId } = useFormField();\n  const body = error ? String(error.message ?? \"\") : children;\n  if (!body) return null;\n  return (\n    <p data-slot=\"form-message\" id={formMessageId} role={error ? \"alert\" : undefined} className={cn(\"m-0 font-sans text-caption leading-body text-danger\", className)} {...props}>\n      {body}\n    </p>\n  );\n}\n\nexport { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useFormField };\n"
    }
  ]
}
