{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "form",
  "title": "Form",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/form/Form.vue",
      "content": "<script lang=\"ts\" setup>\nimport type { HTMLAttributes } from 'vue'\nimport { provide } from 'vue'\nimport { cn } from '@/lib/utils'\nimport { FORM_INSTANCE_INJECTION_KEY, type TanstackFormApi } from './injectionKeys'\n\nconst props = defineProps<{\n  form: TanstackFormApi\n  class?: HTMLAttributes['class']\n}>()\n\nprovide(FORM_INSTANCE_INJECTION_KEY, props.form)\n\nfunction onSubmit(e: Event) {\n  e.preventDefault()\n  e.stopPropagation()\n  void props.form.handleSubmit()\n}\n</script>\n\n<template>\n  <form data-uipkge data-slot=\"form\" :class=\"cn(props.class)\" @submit=\"onSubmit\">\n    <slot />\n  </form>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/form/Form.vue"
    },
    {
      "path": "packages/registry-vue/components/form/FormActions.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = withDefaults(\n  defineProps<{\n    align?: 'left' | 'center' | 'right'\n    gap?: 'sm' | 'md' | 'lg'\n    class?: HTMLAttributes['class']\n  }>(),\n  {\n    align: 'right',\n    gap: 'md',\n  },\n)\n\nconst alignClasses = {\n  left: 'justify-start',\n  center: 'justify-center',\n  right: 'justify-end',\n}\n\nconst gapClasses = {\n  sm: 'gap-2',\n  md: 'gap-3',\n  lg: 'gap-4',\n}\n</script>\n\n<template>\n  <div\n    data-uipkge\n    data-slot=\"form-actions\"\n    :class=\"cn('flex flex-wrap items-center', alignClasses[align], gapClasses[gap], props.class)\"\n  >\n    <slot />\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/form/FormActions.vue"
    },
    {
      "path": "packages/registry-vue/components/form/FormControl.vue",
      "content": "<script lang=\"ts\" setup>\nimport { Slot } from 'reka-ui'\nimport { useFormField } from './useFormField'\n\nconst { error, formItemId, formDescriptionId, formMessageId } = useFormField()\n</script>\n\n<template>\n  <Slot\n    :id=\"formItemId\"\n    data-uipkge\n    data-slot=\"form-control\"\n    :aria-describedby=\"!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`\"\n    :aria-invalid=\"!!error\"\n  >\n    <slot />\n  </Slot>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/form/FormControl.vue"
    },
    {
      "path": "packages/registry-vue/components/form/FormDescription.vue",
      "content": "<script lang=\"ts\" setup>\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\nimport { useFormField } from './useFormField'\n\nconst props = defineProps<{\n  class?: HTMLAttributes['class']\n}>()\n\nconst { formDescriptionId } = useFormField()\n</script>\n\n<template>\n  <p\n    :id=\"formDescriptionId\"\n    data-uipkge\n    data-slot=\"form-description\"\n    :class=\"cn('text-muted-foreground text-sm', props.class)\"\n  >\n    <slot />\n  </p>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/form/FormDescription.vue"
    },
    {
      "path": "packages/registry-vue/components/form/FormField.vue",
      "content": "<script lang=\"ts\" setup>\nimport { computed, inject } from 'vue'\nimport FormFieldInner from './FormFieldInner.vue'\nimport { FORM_INSTANCE_INJECTION_KEY, type TanstackFormApi } from './injectionKeys'\n\nconst props = defineProps<{\n  form?: TanstackFormApi\n  name: string\n  validators?: Record<string, unknown>\n}>()\n\nconst injected = inject(FORM_INSTANCE_INJECTION_KEY, null)\n\nconst resolvedForm = computed<TanstackFormApi>(() => {\n  const f = props.form ?? injected\n  if (!f) throw new Error('<FormField> requires a `form` prop or to be nested inside <Form :form=\"...\">')\n  return f\n})\n</script>\n\n<template>\n  <component :is=\"resolvedForm.Field\" :name=\"props.name\" :validators=\"props.validators\">\n    <template #default=\"{ field }\">\n      <FormFieldInner :field=\"field\">\n        <template #default=\"binding\">\n          <slot v-bind=\"binding\" />\n        </template>\n      </FormFieldInner>\n    </template>\n  </component>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/form/FormField.vue"
    },
    {
      "path": "packages/registry-vue/components/form/FormFieldInner.vue",
      "content": "<script lang=\"ts\" setup>\nimport { computed, onUnmounted, provide, shallowRef, watch } from 'vue'\nimport { FORM_FIELD_INJECTION_KEY } from './injectionKeys'\n\ninterface FieldMeta {\n  errors: Array<unknown>\n  errorMap?: Record<string, Array<unknown> | undefined>\n  isDirty: boolean\n  isTouched: boolean\n  isValid: boolean\n}\n\ninterface FieldApi {\n  name: string\n  state: {\n    value: unknown\n    meta: FieldMeta\n  }\n  store: {\n    subscribe: (cb: () => void) => () => void\n  }\n  handleChange: (v: unknown) => void\n  handleBlur: () => void\n}\n\nconst props = defineProps<{ field: FieldApi }>()\n\nconst snapshot = shallowRef({ value: props.field.state.value, meta: props.field.state.meta })\nlet unsubscribe: (() => void) | null = null\n\nfunction refreshSnapshot() {\n  snapshot.value = { value: props.field.state.value, meta: props.field.state.meta }\n}\n\nfunction subscribe(field: FieldApi) {\n  unsubscribe?.()\n  unsubscribe = field.store.subscribe(refreshSnapshot)\n  refreshSnapshot()\n}\n\nsubscribe(props.field)\nwatch(\n  () => props.field,\n  (next) => subscribe(next),\n)\nonUnmounted(() => unsubscribe?.())\n\nfunction formatError(raw: unknown): string {\n  if (typeof raw === 'string') return raw\n  if (raw && typeof raw === 'object' && 'message' in raw) {\n    return String((raw as { message: unknown }).message)\n  }\n  return String(raw)\n}\n\nconst error = computed(() => {\n  const meta = snapshot.value.meta\n  const flat = meta.errors\n  if (Array.isArray(flat) && flat.length > 0 && flat[0] != null) {\n    return formatError(flat[0])\n  }\n  const map = meta.errorMap ?? {}\n  for (const key of Object.keys(map)) {\n    const arr = map[key]\n    if (Array.isArray(arr) && arr.length > 0 && arr[0] != null) {\n      return formatError(arr[0])\n    }\n  }\n  return undefined\n})\n\nconst valid = computed(() => snapshot.value.meta.isValid)\nconst isDirty = computed(() => snapshot.value.meta.isDirty)\nconst isTouched = computed(() => snapshot.value.meta.isTouched)\n\nprovide(FORM_FIELD_INJECTION_KEY, {\n  name: props.field.name,\n  error,\n  valid,\n  isDirty,\n  isTouched,\n})\n\nconst componentField = computed(() => ({\n  modelValue: snapshot.value.value,\n  'onUpdate:modelValue': (v: unknown) => props.field.handleChange(v),\n  onBlur: () => props.field.handleBlur(),\n  name: props.field.name,\n}))\n</script>\n\n<template>\n  <slot\n    :field=\"props.field\"\n    :component-field=\"componentField\"\n    :error=\"error\"\n    :valid=\"valid\"\n    :is-dirty=\"isDirty\"\n    :is-touched=\"isTouched\"\n  />\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/form/FormFieldInner.vue"
    },
    {
      "path": "packages/registry-vue/components/form/FormItem.vue",
      "content": "<script lang=\"ts\" setup>\nimport type { HTMLAttributes } from 'vue'\nimport { useId } from 'reka-ui'\nimport { computed, provide } from 'vue'\nimport { cn } from '@/lib/utils'\nimport { FORM_ITEM_INJECTION_KEY } from './injectionKeys'\nimport FormLabel from './FormLabel.vue'\nimport type { FormStatus } from './types'\n\nconst props = defineProps<{\n  class?: HTMLAttributes['class']\n  label?: string\n  required?: boolean\n  description?: string\n  status?: FormStatus\n  help?: string\n  layout?: 'vertical' | 'horizontal'\n  labelWidth?: string\n}>()\n\nconst id = useId()\nprovide(FORM_ITEM_INJECTION_KEY, id)\n\nconst statusBorderClass = computed(() => {\n  switch (props.status) {\n    case 'error':\n      return 'border-destructive focus-within:border-destructive focus-within:ring-destructive/20'\n    case 'warning':\n      return 'border-warning focus-within:border-warning focus-within:ring-warning/20'\n    case 'success':\n      return 'border-success focus-within:border-success focus-within:ring-success/20'\n    default:\n      return ''\n  }\n})\n\nconst isHorizontal = computed(() => props.layout === 'horizontal')\n</script>\n\n<template>\n  <div\n    data-uipkge\n    data-slot=\"form-item\"\n    :class=\"\n      cn(\n        'grid gap-1.5',\n        isHorizontal && 'grid-cols-[var(--label-width,140px)_1fr] items-start gap-x-4 gap-y-0',\n        props.class,\n      )\n    \"\n    :style=\"labelWidth ? { '--label-width': labelWidth } : undefined\"\n  >\n    <div v-if=\"label || $slots.label\" class=\"flex items-center gap-1\">\n      <FormLabel v-if=\"label\" :for=\"id\">{{ label }}</FormLabel>\n      <slot name=\"label\" />\n      <span v-if=\"required\" class=\"text-destructive text-sm\">*</span>\n    </div>\n\n    <div class=\"space-y-1\">\n      <slot />\n      <p v-if=\"description\" class=\"text-muted-foreground text-xs\">{{ description }}</p>\n      <p\n        v-if=\"help\"\n        class=\"text-xs\"\n        :class=\"{\n          'text-destructive': status === 'error',\n          'text-warning': status === 'warning',\n          'text-success': status === 'success',\n          'text-muted-foreground': !status,\n        }\"\n      >\n        {{ help }}\n      </p>\n    </div>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/form/FormItem.vue"
    },
    {
      "path": "packages/registry-vue/components/form/FormLabel.vue",
      "content": "<script lang=\"ts\" setup>\nimport type { LabelProps } from 'reka-ui'\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\nimport { Label } from '@/components/ui/label'\nimport { useFormField } from './useFormField'\n\nconst props = defineProps<LabelProps & { class?: HTMLAttributes['class'] }>()\n\nconst { error, formItemId } = useFormField()\n</script>\n\n<template>\n  <Label\n    data-uipkge\n    data-slot=\"form-label\"\n    :data-error=\"!!error\"\n    :class=\"cn('data-[error=true]:text-destructive', props.class)\"\n    :for=\"formItemId\"\n  >\n    <slot />\n  </Label>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/form/FormLabel.vue"
    },
    {
      "path": "packages/registry-vue/components/form/FormMessage.vue",
      "content": "<script lang=\"ts\" setup>\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\nimport { useFormField } from './useFormField'\n\nconst props = defineProps<{\n  class?: HTMLAttributes['class']\n}>()\n\nconst { error, formMessageId } = useFormField()\n</script>\n\n<template>\n  <p\n    v-if=\"error\"\n    :id=\"formMessageId\"\n    data-uipkge\n    data-slot=\"form-message\"\n    :class=\"cn('text-destructive text-sm', props.class)\"\n  >\n    {{ error }}\n  </p>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/form/FormMessage.vue"
    },
    {
      "path": "packages/registry-vue/components/form/FormSection.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<{\n  title?: string\n  subtitle?: string\n  description?: string\n  divider?: boolean\n  headingLevel?: 'h2' | 'h3' | 'h4' | 'h5'\n  class?: HTMLAttributes['class']\n}>()\n</script>\n\n<template>\n  <div data-uipkge data-slot=\"form-section\" :class=\"cn('space-y-3', props.class)\">\n    <div v-if=\"divider || title || subtitle\" :class=\"divider && 'border-t pt-4'\">\n      <div v-if=\"title || subtitle\" class=\"space-y-1\">\n        <component :is=\"props.headingLevel ?? 'h4'\" v-if=\"title\" class=\"text-sm font-semibold\">{{ title }}</component>\n        <p v-if=\"subtitle\" class=\"text-muted-foreground text-xs\">{{ subtitle }}</p>\n      </div>\n    </div>\n    <p v-if=\"description\" class=\"text-muted-foreground text-xs\">{{ description }}</p>\n    <slot />\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/form/FormSection.vue"
    },
    {
      "path": "packages/registry-vue/components/form/FormStatus.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes, Component } from 'vue'\nimport { CircleAlert, TriangleAlert, CircleCheck } from 'lucide-vue-next'\nimport { cn } from '@/lib/utils'\nimport type { FormStatus } from './types'\n\nconst props = defineProps<{\n  class?: HTMLAttributes['class']\n  status: FormStatus\n  message: string\n}>()\n\nconst statusConfig: Record<FormStatus, { icon: Component; container: string }> = {\n  error: {\n    icon: CircleAlert,\n    container: 'bg-destructive/10 text-destructive border-destructive/20',\n  },\n  warning: {\n    icon: TriangleAlert,\n    container: 'bg-warning/10 text-warning border-warning/30',\n  },\n  success: {\n    icon: CircleCheck,\n    container: 'bg-success/10 text-success border-success/30',\n  },\n}\n</script>\n\n<template>\n  <div\n    v-if=\"status\"\n    data-uipkge\n    data-slot=\"form-status\"\n    :class=\"\n      cn('flex items-center gap-2 rounded-md border px-3 py-2 text-sm', statusConfig[status].container, props.class)\n    \"\n  >\n    <component :is=\"statusConfig[status].icon\" class=\"size-4 shrink-0\" />\n    <span>{{ message }}</span>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/form/FormStatus.vue"
    },
    {
      "path": "packages/registry-vue/components/form/index.ts",
      "content": "export { default as Form } from './Form.vue'\nexport { default as FormField } from './FormField.vue'\nexport { default as FormControl } from './FormControl.vue'\nexport { default as FormDescription } from './FormDescription.vue'\nexport { default as FormItem } from './FormItem.vue'\nexport { default as FormLabel } from './FormLabel.vue'\nexport { default as FormMessage } from './FormMessage.vue'\nexport { default as FormActions } from './FormActions.vue'\nexport { default as FormSection } from './FormSection.vue'\nexport { default as FormStatus } from './FormStatus.vue'\nexport { useFormField } from './useFormField'\nexport { FORM_ITEM_INJECTION_KEY, FORM_FIELD_INJECTION_KEY } from './injectionKeys'\nexport type { FormFieldContext } from './injectionKeys'\nexport type { FormStatus as FormStatusValue } from './types'\nexport { useForm } from '@tanstack/vue-form'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/form/index.ts"
    },
    {
      "path": "packages/registry-vue/components/form/injectionKeys.ts",
      "content": "import type { ComputedRef, InjectionKey } from 'vue'\n\nexport const FORM_ITEM_INJECTION_KEY = Symbol() as InjectionKey<string>\n\nexport interface FormFieldContext {\n  name: string\n  error: ComputedRef<string | undefined>\n  valid: ComputedRef<boolean>\n  isDirty: ComputedRef<boolean>\n  isTouched: ComputedRef<boolean>\n}\n\nexport const FORM_FIELD_INJECTION_KEY = Symbol() as InjectionKey<FormFieldContext>\n\nexport interface TanstackFormApi {\n  Field: unknown\n  handleSubmit: () => void | Promise<void>\n}\n\nexport const FORM_INSTANCE_INJECTION_KEY = Symbol() as InjectionKey<TanstackFormApi>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/form/injectionKeys.ts"
    },
    {
      "path": "packages/registry-vue/components/form/types.ts",
      "content": "export type FormStatus = 'error' | 'warning' | 'success' | null | undefined\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/form/types.ts"
    },
    {
      "path": "packages/registry-vue/components/form/useFormField.ts",
      "content": "import { computed, inject } from 'vue'\nimport { FORM_FIELD_INJECTION_KEY, FORM_ITEM_INJECTION_KEY } from './injectionKeys'\n\nexport function useFormField() {\n  const fieldContext = inject(FORM_FIELD_INJECTION_KEY)\n  const fieldItemContext = inject(FORM_ITEM_INJECTION_KEY)\n\n  if (!fieldContext) throw new Error('useFormField should be used within <FormField>')\n\n  const id = fieldItemContext\n\n  return {\n    id,\n    name: computed(() => fieldContext.name),\n    formItemId: `${id}-form-item`,\n    formDescriptionId: `${id}-form-item-description`,\n    formMessageId: `${id}-form-item-message`,\n    valid: fieldContext.valid,\n    isDirty: fieldContext.isDirty,\n    isTouched: fieldContext.isTouched,\n    error: fieldContext.error,\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/form/useFormField.ts"
    }
  ],
  "dependencies": [
    "reka-ui",
    "@tanstack/vue-form",
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/label.json"
  ],
  "description": "Zod-first form block built on TanStack Vue Form. Wires field labels, descriptions, error messages, and validation together; bind a field once and the rest is automatic.",
  "categories": [
    "form"
  ]
}