{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "form",
  "title": "Form",
  "type": "registry:ui",
  "description": "Zod-first form block built on React Hook Form. Wires field labels, descriptions, error messages, and validation together; bind a field once and the rest is automatic.",
  "categories": [
    "form"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/form/form.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { styleMap } from 'lit/directives/style-map.js'\nimport { CircleAlert, CircleCheck, TriangleAlert } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\n\nexport type FormStatusValue = 'error' | 'warning' | 'success' | null | undefined\n\n/* -------------------------------------------------------------------------- */\n/*                                  UipForm                                   */\n/* -------------------------------------------------------------------------- */\n\nexport class UipForm extends LitElement {\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'form')\n  }\n\n  render() {\n    return html`<form part=\"base\" class=\"space-y-4\" @submit=${this.onSubmit}><slot></slot></form>`\n  }\n\n  private onSubmit(e: Event) {\n    // Let consumers handle submit on the element or native form\n    this.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))\n  }\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                UipFormItem                                 */\n/* -------------------------------------------------------------------------- */\n\nexport class UipFormItem extends LitElement {\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    label: {},\n    required: { type: Boolean, reflect: true },\n    description: {},\n    status: { reflect: true },\n    help: {},\n    layout: { reflect: true },\n    labelWidth: { attribute: 'label-width' },\n  }\n\n  label?: string\n  required = false\n  description?: string\n  status?: FormStatusValue\n  help?: string\n  layout: 'vertical' | 'horizontal' = 'vertical'\n  labelWidth?: string\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'form-item')\n  }\n\n  render() {\n    const isHorizontal = this.layout === 'horizontal'\n\n    return html`\n      <div\n        part=\"base\"\n        class=${cn(\n          'grid gap-1.5',\n          isHorizontal && 'grid-cols-[var(--label-width,140px)_1fr] items-start gap-x-4 gap-y-0',\n        )}\n        style=${styleMap(this.labelWidth ? { '--label-width': this.labelWidth } : {})}\n      >\n        ${this.label || this.required\n          ? html`\n              <div class=\"flex items-center gap-1\">\n                ${this.label\n                  ? html`<label\n                      part=\"label\"\n                      class=${cn('text-sm font-medium leading-none select-none', this.status === 'error' && 'text-destructive')}\n                    >\n                      ${this.label}\n                    </label>`\n                  : nothing}\n                ${this.required\n                  ? html`<span class=\"text-destructive text-sm\" aria-hidden=\"true\">*</span>`\n                  : nothing}\n              </div>\n            `\n          : nothing}\n        <div\n          part=\"control-wrapper\"\n          class=${cn(\n            'space-y-1',\n            this.status === 'error' &&\n              '[&_input]:border-destructive [&_textarea]:border-destructive [&_button]:border-destructive',\n            this.status === 'warning' &&\n              '[&_input]:border-warning [&_textarea]:border-warning [&_button]:border-warning',\n            this.status === 'success' &&\n              '[&_input]:border-success [&_textarea]:border-success [&_button]:border-success',\n          )}\n        >\n          <slot></slot>\n          ${this.description\n            ? html`<p part=\"description\" class=\"text-muted-foreground text-xs\">${this.description}</p>`\n            : nothing}\n          ${this.help\n            ? html`<p\n                part=\"help\"\n                role=${this.status === 'error' ? 'alert' : nothing}\n                class=${cn(\n                  'text-xs',\n                  this.status === 'error' && 'text-destructive',\n                  this.status === 'warning' && 'text-warning',\n                  this.status === 'success' && 'text-success',\n                  !this.status && 'text-muted-foreground',\n                )}\n              >\n                ${this.help}\n              </p>`\n            : nothing}\n        </div>\n      </div>\n    `\n  }\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                UipFormLabel                                */\n/* -------------------------------------------------------------------------- */\n\nexport class UipFormLabel extends LitElement {\n  static styles = [tailwind, css`:host { display: inline-block; }`]\n\n  static properties = {\n    htmlFor: { attribute: 'for' },\n    error: { type: Boolean, reflect: true },\n  }\n\n  htmlFor?: string\n  error = false\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'form-label')\n  }\n\n  render() {\n    return html`\n      <label\n        part=\"label\"\n        for=${this.htmlFor ?? nothing}\n        data-error=${this.error ? 'true' : 'false'}\n        class=${cn(\n          'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',\n          this.error && 'text-destructive',\n        )}\n      >\n        <slot></slot>\n      </label>\n    `\n  }\n}\n\n/* -------------------------------------------------------------------------- */\n/*                            UipFormDescription                             */\n/* -------------------------------------------------------------------------- */\n\nexport class UipFormDescription extends LitElement {\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'form-description')\n  }\n\n  render() {\n    return html`<p part=\"base\" class=\"text-muted-foreground text-sm\"><slot></slot></p>`\n  }\n}\n\n/* -------------------------------------------------------------------------- */\n/*                               UipFormMessage                               */\n/* -------------------------------------------------------------------------- */\n\nexport class UipFormMessage extends LitElement {\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'form-message')\n  }\n\n  render() {\n    return html`<p part=\"base\" role=\"alert\" class=\"text-destructive text-sm\"><slot></slot></p>`\n  }\n}\n\n/* -------------------------------------------------------------------------- */\n/*                               UipFormSection                               */\n/* -------------------------------------------------------------------------- */\n\nexport class UipFormSection extends LitElement {\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    sectionTitle: { attribute: 'title' },\n    subtitle: {},\n    description: {},\n    divider: { type: Boolean, reflect: true },\n    headingLevel: { attribute: 'heading-level' },\n  }\n\n  sectionTitle?: string\n  subtitle?: string\n  description?: string\n  divider = false\n  headingLevel: 'h2' | 'h3' | 'h4' | 'h5' = 'h4'\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'form-section')\n  }\n\n  private renderHeading() {\n    const title = this.sectionTitle || this.getAttribute('title')\n    if (!title) return nothing\n    switch (this.headingLevel) {\n      case 'h2':\n        return html`<h2 class=\"text-sm font-semibold\">${title}</h2>`\n      case 'h3':\n        return html`<h3 class=\"text-sm font-semibold\">${title}</h3>`\n      case 'h5':\n        return html`<h5 class=\"text-sm font-semibold\">${title}</h5>`\n      default:\n        return html`<h4 class=\"text-sm font-semibold\">${title}</h4>`\n    }\n  }\n\n  render() {\n    const title = this.sectionTitle || this.getAttribute('title')\n    return html`\n      <div part=\"base\" class=\"space-y-3\">\n        ${this.divider || title || this.subtitle\n          ? html`\n              <div class=${cn(this.divider && 'border-t pt-4')}>\n                ${title || this.subtitle\n                  ? html`\n                      <div class=\"space-y-1\">\n                        ${this.renderHeading()}\n                        ${this.subtitle\n                          ? html`<p class=\"text-muted-foreground text-xs\">${this.subtitle}</p>`\n                          : nothing}\n                      </div>\n                    `\n                  : nothing}\n              </div>\n            `\n          : nothing}\n        ${this.description\n          ? html`<p class=\"text-muted-foreground text-xs\">${this.description}</p>`\n          : nothing}\n        <slot></slot>\n      </div>\n    `\n  }\n}\n\n/* -------------------------------------------------------------------------- */\n/*                               UipFormActions                               */\n/* -------------------------------------------------------------------------- */\n\nconst alignClasses = {\n  left: 'justify-start',\n  center: 'justify-center',\n  right: 'justify-end',\n} as const\n\nconst gapClasses = {\n  sm: 'gap-2',\n  md: 'gap-3',\n  lg: 'gap-4',\n} as const\n\nexport class UipFormActions extends LitElement {\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    align: { reflect: true },\n    gap: { reflect: true },\n  }\n\n  align: 'left' | 'center' | 'right' = 'right'\n  gap: 'sm' | 'md' | 'lg' = 'md'\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'form-actions')\n  }\n\n  render() {\n    return html`\n      <div\n        part=\"base\"\n        class=${cn(\n          'flex flex-wrap items-center',\n          alignClasses[this.align] ?? alignClasses.right,\n          gapClasses[this.gap] ?? gapClasses.md,\n        )}\n      >\n        <slot></slot>\n      </div>\n    `\n  }\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                UipFormStatus                               */\n/* -------------------------------------------------------------------------- */\n\nconst statusContainer = {\n  error: 'bg-destructive/10 text-destructive border-destructive/20',\n  warning: 'bg-warning/10 text-warning border-warning/30',\n  success: 'bg-success/10 text-success border-success/30',\n} as const\n\nexport class UipFormStatus extends LitElement {\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    status: { reflect: true },\n    message: {},\n  }\n\n  status: 'error' | 'warning' | 'success' = 'error'\n  message = ''\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'form-status')\n  }\n\n  render() {\n    const isError = this.status === 'error'\n    const isWarning = this.status === 'warning'\n\n    return html`\n      <div\n        part=\"base\"\n        role=${isError ? 'alert' : 'status'}\n        class=${cn(\n          'flex items-center gap-2 rounded-md border px-3 py-2 text-sm',\n          statusContainer[this.status] ?? statusContainer.error,\n        )}\n      >\n        ${isError\n          ? icon(CircleAlert, 'circle-alert', 'size-4 shrink-0')\n          : isWarning\n            ? icon(TriangleAlert, 'triangle-alert', 'size-4 shrink-0')\n            : icon(CircleCheck, 'circle-check', 'size-4 shrink-0')}\n        <span>${this.message}</span>\n        <slot></slot>\n      </div>\n    `\n  }\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                Registration                                */\n/* -------------------------------------------------------------------------- */\n\ncustomElements.get('uip-form') || customElements.define('uip-form', UipForm)\ncustomElements.get('uip-form-item') || customElements.define('uip-form-item', UipFormItem)\ncustomElements.get('uip-form-label') || customElements.define('uip-form-label', UipFormLabel)\ncustomElements.get('uip-form-description') || customElements.define('uip-form-description', UipFormDescription)\ncustomElements.get('uip-form-message') || customElements.define('uip-form-message', UipFormMessage)\ncustomElements.get('uip-form-section') || customElements.define('uip-form-section', UipFormSection)\ncustomElements.get('uip-form-actions') || customElements.define('uip-form-actions', UipFormActions)\ncustomElements.get('uip-form-status') || customElements.define('uip-form-status', UipFormStatus)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-form': UipForm\n    'uip-form-item': UipFormItem\n    'uip-form-label': UipFormLabel\n    'uip-form-description': UipFormDescription\n    'uip-form-message': UipFormMessage\n    'uip-form-section': UipFormSection\n    'uip-form-actions': UipFormActions\n    'uip-form-status': UipFormStatus\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/form/form.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
