{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "form-layouts",
  "title": "Form Layouts",
  "type": "registry:block",
  "description": "Structured create/edit entity form (\"New project\"): stacked SectionCards for project details (name with live slug preview, description with char counter), configuration (visibility Select, start date, budget), and access (link vs restricted RadioGroup, notification/comment Switch rows) with a sticky-feel footer bar. Create stays disabled until the name is filled; validation hints appear under empty required fields after the first submit attempt.",
  "categories": [
    "layout",
    "app",
    "forms"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/blocks/form-layouts/form-layouts.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { Circle, Globe, Link2, Lock } from 'lucide'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\n// Side-effect imports: register the composed elements for standalone use\n// (consumers who install only this block still get working primitives).\nimport '../../components/button/button'\nimport '../../components/input/input'\nimport type { UipInput } from '../../components/input/input'\nimport '../../components/label/label'\nimport '../../components/section-card/section-card'\nimport '../../components/select/select'\nimport type { UipSelect } from '../../components/select/select'\nimport '../../components/switch/switch'\nimport type { UipSwitch } from '../../components/switch/switch'\nimport '../../components/textarea/textarea'\nimport type { UipTextarea } from '../../components/textarea/textarea'\n\nconst DESCRIPTION_LIMIT = 280\n\ntype Access = 'link' | 'restricted'\n\nconst radioCircle =\n  'border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square shrink-0 rounded-full border shadow-sm transition-colors duration-200 outline-none focus-visible:ring-[3px] size-4 data-[state=checked]:border-primary mt-0.5'\n\n/**\n * <uip-form-layouts> — the registry FormLayouts block as a web component.\n *\n * Class strings are React's FormLayouts verbatim. React state (all fields)\n * becomes @state fields; the slug preview, char counter, and validation hint\n * derive the same way. `SectionCard`'s `title` is `heading` here (`title` is\n * taken by HTMLElement). No props, no events: edit the markup in place\n * (React ships no callbacks either).\n *\n * Deviation: the Access plan cards keep React's card visuals 1:1 but the\n * radio circle is block-local markup (React's `RadioGroup`/`RadioGroupItem`\n * only render plain labels, so they can't host the rich card content). The\n * cards form a real `radiogroup` (roving tabindex, arrows/Home/End) with\n * Radix's circle + indicator classes verbatim.\n */\nexport class UipFormLayouts extends LitElement {\n  static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }\n  // :host display can't be a utility class (the host has no template of its own).\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    name: { state: true },\n    description: { state: true },\n    visibility: { state: true },\n    startDate: { state: true },\n    budget: { state: true },\n    access: { state: true },\n    notifications: { state: true },\n    comments: { state: true },\n    submitted: { state: true },\n  }\n\n  private name = ''\n  private description = ''\n  private visibility = 'internal'\n  private startDate = ''\n  private budget = ''\n  private access: Access = 'link'\n  private notifications = true\n  private comments = true\n  private submitted = 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-layouts')\n  }\n\n  private get slug() {\n    return (\n      this.name\n        .trim()\n        .toLowerCase()\n        .replace(/[^a-z0-9]+/g, '-')\n        .replace(/^-+|-+$/g, '') || 'your-project'\n    )\n  }\n\n  private handleSubmit(e: Event) {\n    e.preventDefault()\n    this.submitted = true\n    if (this.name.trim() === '') return\n  }\n\n  private onAccessKey(e: KeyboardEvent, value: Access) {\n    const order: Access[] = ['link', 'restricted']\n    const at = order.indexOf(value)\n    let next: Access | null = null\n    if (e.key === 'ArrowDown' || e.key === 'ArrowRight') next = order[(at + 1) % order.length] ?? null\n    else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') next = order[(at + order.length - 1) % order.length] ?? null\n    else if (e.key === 'Home') next = 'link'\n    else if (e.key === 'End') next = 'restricted'\n    else if (e.key === ' ' || e.key === 'Enter') next = value\n    if (!next) return\n    e.preventDefault()\n    this.access = next\n    if (e.key !== ' ' && e.key !== 'Enter') {\n      this.renderRoot.querySelector<HTMLElement>(`[data-access=\"${next}\"]`)?.focus()\n    }\n  }\n\n  private accessCard(value: Access, titleText: string, desc: string, iconNode: typeof Link2, iconName: string) {\n    const checked = this.access === value\n    return html`\n      <div\n        data-access=${value}\n        role=\"radio\"\n        aria-checked=${checked ? 'true' : 'false'}\n        tabindex=${checked ? '0' : '-1'}\n        class=\"hover:bg-accent/50 has-data-[state=checked]:border-primary flex items-start gap-3 rounded-lg border p-3 transition-colors\"\n        @click=${() => (this.access = value)}\n        @keydown=${(e: KeyboardEvent) => this.onAccessKey(e, value)}\n      >\n        <span data-uipkge=\"\" data-slot=\"radio-group-item\" data-state=${checked ? 'checked' : 'unchecked'} class=${radioCircle}>\n          ${checked\n            ? html`<span data-uipkge=\"\" data-slot=\"radio-group-indicator\" class=\"relative flex items-center justify-center\">\n                ${icon(Circle, 'circle', 'absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 fill-current text-current size-2')}\n              </span>`\n            : nothing}\n        </span>\n        <div class=\"cursor-pointer select-none\">\n          <span class=\"flex items-center gap-1.5 text-sm font-medium\">\n            ${icon(iconNode, iconName, 'size-4')} ${titleText}\n          </span>\n          <span class=\"text-muted-foreground mt-0.5 block text-xs\">${desc}</span>\n        </div>\n      </div>\n    `\n  }\n\n  render() {\n    const showNameError = this.submitted && this.name.trim() === ''\n    return html`\n      <div data-slot=\"form-layouts\" class=\"max-w-2xl\">\n        <form class=\"space-y-6\" novalidate @submit=${this.handleSubmit}>\n          <uip-section-card heading=\"Project details\" description=\"Name your project and describe what it covers.\">\n            <div class=\"space-y-4\">\n              <div class=\"space-y-2\">\n                <uip-label for=\"fl-name\">Name</uip-label>\n                <uip-input\n                  id=\"fl-name\"\n                  .value=${this.name}\n                  placeholder=\"Q3 launch site\"\n                  autocomplete=\"off\"\n                  aria-invalid=${showNameError ? 'true' : nothing}\n                  @input=${(e: Event) => {\n                    this.name = (e.target as UipInput).value\n                  }}\n                ></uip-input>\n                ${showNameError\n                  ? html`<p role=\"alert\" class=\"text-destructive text-xs\">Project name is required.</p>`\n                  : nothing}\n              </div>\n              <div class=\"border-border bg-muted/40 flex items-center gap-2 rounded-md border px-3 py-2\">\n                ${icon(Globe, 'globe', 'text-muted-foreground size-4 shrink-0')}\n                <p class=\"min-w-0 truncate text-xs\">\n                  <span class=\"text-muted-foreground\">acme.uipkge.app/</span>\n                  <span class=\"text-foreground font-medium\">${this.slug}</span>\n                </p>\n              </div>\n              <div class=\"space-y-2\">\n                <uip-label for=\"fl-description\">Description</uip-label>\n                <uip-textarea\n                  id=\"fl-description\"\n                  .value=${this.description}\n                  rows=\"3\"\n                  placeholder=\"What is this project about?\"\n                  @input=${(e: Event) => {\n                    this.description = (e.target as UipTextarea).value ?? ''\n                  }}\n                ></uip-textarea>\n                <p\n                  class=${`text-right text-xs ${this.description.length > DESCRIPTION_LIMIT ? 'text-destructive' : 'text-muted-foreground'}`}\n                >\n                  ${this.description.length}/${DESCRIPTION_LIMIT}\n                </p>\n              </div>\n            </div>\n          </uip-section-card>\n\n          <uip-section-card heading=\"Configuration\" description=\"Visibility, schedule and budget for the project.\">\n            <div class=\"grid gap-4 sm:grid-cols-3\">\n              <div class=\"space-y-2\">\n                <uip-label for=\"fl-visibility\">Visibility</uip-label>\n                <uip-select\n                  id=\"fl-visibility\"\n                  class=\"w-full\"\n                  .value=${this.visibility}\n                  @change=${(e: Event) => {\n                    this.visibility = (e.target as UipSelect).value\n                  }}\n                >\n                  <option value=\"public\">Public</option>\n                  <option value=\"internal\">Internal</option>\n                  <option value=\"private\">Private</option>\n                </uip-select>\n              </div>\n              <div class=\"space-y-2\">\n                <uip-label for=\"fl-start-date\">Start date</uip-label>\n                <uip-input\n                  id=\"fl-start-date\"\n                  type=\"date\"\n                  .value=${this.startDate}\n                  @input=${(e: Event) => {\n                    this.startDate = (e.target as UipInput).value\n                  }}\n                ></uip-input>\n              </div>\n              <div class=\"space-y-2\">\n                <uip-label for=\"fl-budget\">Budget</uip-label>\n                <uip-input\n                  id=\"fl-budget\"\n                  type=\"number\"\n                  min=\"0\"\n                  step=\"500\"\n                  class=\"text-left\"\n                  .value=${this.budget}\n                  @input=${(e: Event) => {\n                    this.budget = (e.target as UipInput).value\n                  }}\n                ></uip-input>\n              </div>\n            </div>\n          </uip-section-card>\n\n          <uip-section-card heading=\"Access\" description=\"Decide who can open the project and what they can do.\">\n            <div class=\"space-y-5\">\n              <div role=\"radiogroup\" class=\"gap-3\" aria-label=\"Access\">\n                ${this.accessCard('link', 'Anyone with the link', 'Guests can view without signing in.', Link2, 'link-2')}\n                ${this.accessCard('restricted', 'Restricted', 'Only invited members can open the project.', Lock, 'lock')}\n              </div>\n\n              <div class=\"divide-y rounded-lg border\">\n                <div class=\"flex items-center justify-between gap-4 p-4\">\n                  <div class=\"min-w-0\">\n                    <p class=\"text-sm font-medium\">Notifications</p>\n                    <p class=\"text-muted-foreground mt-0.5 text-xs\">Email the team on status changes and mentions.</p>\n                  </div>\n                  <uip-switch\n                    .checked=${this.notifications}\n                    aria-label=\"Toggle notifications\"\n                    @change=${(e: Event) => {\n                      this.notifications = (e.target as UipSwitch).checked\n                    }}\n                  ></uip-switch>\n                </div>\n                <div class=\"flex items-center justify-between gap-4 p-4\">\n                  <div class=\"min-w-0\">\n                    <p class=\"text-sm font-medium\">Comments</p>\n                    <p class=\"text-muted-foreground mt-0.5 text-xs\">Let viewers comment on tasks and updates.</p>\n                  </div>\n                  <uip-switch\n                    .checked=${this.comments}\n                    aria-label=\"Toggle comments\"\n                    @change=${(e: Event) => {\n                      this.comments = (e.target as UipSwitch).checked\n                    }}\n                  ></uip-switch>\n                </div>\n              </div>\n            </div>\n          </uip-section-card>\n\n          <div class=\"bg-background/80 border-border sticky bottom-0 -mx-1 flex items-center justify-end gap-2 rounded-b-xl border-t px-1 py-4 backdrop-blur\">\n            <uip-button type=\"button\" variant=\"ghost\" size=\"sm\"> Cancel </uip-button>\n            <uip-button type=\"submit\" size=\"sm\" ?disabled=${this.name.trim() === ''}> Create project </uip-button>\n          </div>\n        </form>\n      </div>\n    `\n  }\n}\n\ncustomElements.get('uip-form-layouts') || customElements.define('uip-form-layouts', UipFormLayouts)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-form-layouts': UipFormLayouts\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/blocks/form-layouts/form-layouts.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/button.json",
    "https://uipkge.dev/r/lit/input.json",
    "https://uipkge.dev/r/lit/label.json",
    "https://uipkge.dev/r/lit/lit-core.json",
    "https://uipkge.dev/r/lit/section-card.json",
    "https://uipkge.dev/r/lit/select.json",
    "https://uipkge.dev/r/lit/switch.json",
    "https://uipkge.dev/r/lit/textarea.json"
  ]
}
