{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "onboarding-wizard",
  "title": "Onboarding Wizard",
  "type": "registry:block",
  "description": "Multi-step workspace setup flow in a SectionCard: a four-step Stepper (Workspace → Team → Preferences → Done) with check icons on completed steps, a workspace-name input with live URL slug preview, teammate invites as removable email chips with a skip link, preference Switch rows, and a success panel with a glowing check circle. Footer carries Back/Continue actions plus a Step X of 4 counter. Data is stubbed inline — wire your API of choice.",
  "categories": [
    "auth",
    "app",
    "onboarding"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/blocks/onboarding-wizard/onboarding-wizard.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { Check, ChevronLeft, ChevronRight, Globe, Plus, X } 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/badge/badge'\nimport '../../components/button/button'\nimport '../../components/input/input'\nimport '../../components/section-card/section-card'\nimport '../../components/stepper/stepper'\nimport '../../components/switch/switch'\nimport type { UipInput } from '../../components/input/input'\n\ninterface WizardPreference {\n  id: string\n  label: string\n  description: string\n  enabled: boolean\n}\n\nconst STEPS = [{ title: 'Workspace' }, { title: 'Team' }, { title: 'Preferences' }, { title: 'Done' }]\n\nconst DEFAULT_PREFERENCES: WizardPreference[] = [\n  { id: 'digests', label: 'Email digests', description: 'A daily summary of workspace activity.', enabled: true },\n  {\n    id: 'updates',\n    label: 'Product updates',\n    description: 'Occasional notes about new features and improvements.',\n    enabled: false,\n  },\n  {\n    id: 'reports',\n    label: 'Weekly reports',\n    description: 'Usage analytics delivered every Monday morning.',\n    enabled: true,\n  },\n]\n\n/**\n * <uip-onboarding-wizard> — the registry OnboardingWizard block as a web component.\n *\n * Class strings are React's OnboardingWizard verbatim. React state becomes\n * @state fields. `initial-step` (React `initialStep`) jumps straight to a\n * step; `initial-workspace-name` and `initial-invites` (comma-separated)\n * pre-fill state. React's `className` maps to classes on the host. The slug\n * preview, invite chips, and preference switches behave as in React.\n */\nexport class UipOnboardingWizard 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    initialStep: { type: Number, attribute: 'initial-step' },\n    initialWorkspaceName: { attribute: 'initial-workspace-name' },\n    initialInvites: { attribute: 'initial-invites' },\n    step: { state: true },\n    workspaceName: { state: true },\n    invites: { state: true },\n    inviteDraft: { state: true },\n    inviteInvalid: { state: true },\n    preferences: { state: true },\n  }\n\n  initialStep = 1\n  initialWorkspaceName = 'Acme Inc'\n  initialInvites = ''\n  private step = 1\n  private workspaceName = 'Acme Inc'\n  private invites: string[] = []\n  private inviteDraft = ''\n  private inviteInvalid = false\n  private preferences: WizardPreference[] = DEFAULT_PREFERENCES.map((p) => ({ ...p }))\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', 'onboarding-wizard')\n    this.step = this.initialStep\n    this.workspaceName = this.initialWorkspaceName\n    this.invites = this.initialInvites\n      ? this.initialInvites\n          .split(',')\n          .map((s) => s.trim())\n          .filter(Boolean)\n      : []\n  }\n\n  private get slug(): string {\n    return (\n      this.workspaceName\n        .trim()\n        .toLowerCase()\n        .replace(/[^a-z0-9]+/g, '-')\n        .replace(/^-+|-+$/g, '') || 'your-workspace'\n    )\n  }\n\n  private addInvite() {\n    const email = this.inviteDraft.trim()\n    if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email) || this.invites.includes(email)) {\n      this.inviteInvalid = true\n      return\n    }\n    this.inviteInvalid = false\n    this.invites = [...this.invites, email]\n    this.inviteDraft = ''\n  }\n\n  private removeInvite(email: string) {\n    this.invites = this.invites.filter((e) => e !== email)\n  }\n\n  private onStepperInput(e: CustomEvent<{ value: number }>) {\n    // Free backward navigation; forward goes through the footer button only.\n    if (e.detail.value < this.step && this.step !== 4) this.step = e.detail.value\n  }\n\n  private goNext() {\n    if (this.step < 4) this.step += 1\n  }\n\n  private reset() {\n    this.step = 1\n    this.workspaceName = ''\n    this.invites = []\n    this.inviteDraft = ''\n  }\n\n  render() {\n    return html`\n      <uip-section-card\n        data-slot=\"onboarding-wizard\"\n        heading=\"Set up your workspace\"\n        description=\"Four quick steps and your team is ready to collaborate.\"\n      >\n        <uip-stepper\n          .steps=${STEPS}\n          value=${this.step}\n          class=\"mb-6\"\n          @value-change=${this.onStepperInput}\n        ></uip-stepper>\n\n        ${this.step === 1\n          ? html`\n              <div class=\"space-y-4\">\n                <div class=\"space-y-2\">\n                  <label for=\"onboarding-workspace-name\" class=\"text-sm font-medium\">Workspace name</label>\n                  <uip-input\n                    id=\"onboarding-workspace-name\"\n                    .value=${this.workspaceName}\n                    placeholder=\"Acme Inc\"\n                    autocomplete=\"off\"\n                    @input=${(e: Event) => {\n                      this.workspaceName = (e.target as UipInput).value\n                    }}\n                  ></uip-input>\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\">uipkge.dev/</span>\n                    <span class=\"text-foreground font-medium\">${this.slug}</span>\n                  </p>\n                </div>\n              </div>\n            `\n          : nothing}\n        ${this.step === 2\n          ? html`\n              <div class=\"space-y-4\">\n                <form\n                  class=\"flex gap-2\"\n                  @submit=${(e: Event) => {\n                    e.preventDefault()\n                    this.addInvite()\n                  }}\n                >\n                  <uip-input\n                    .value=${this.inviteDraft}\n                    type=\"email\"\n                    placeholder=\"teammate@company.com\"\n                    status=${this.inviteInvalid ? 'error' : nothing}\n                    class=\"flex-1\"\n                    @input=${(e: Event) => {\n                      this.inviteDraft = (e.target as UipInput).value\n                      this.inviteInvalid = false\n                    }}\n                  ></uip-input>\n                  <uip-button type=\"submit\" variant=\"outline\" size=\"sm\" class=\"shrink-0\">\n                    ${icon(Plus, 'plus')} Add\n                  </uip-button>\n                </form>\n                ${this.inviteInvalid\n                  ? html`<p role=\"alert\" class=\"text-destructive text-xs\">Enter a valid email address.</p>`\n                  : nothing}\n                ${this.invites.length > 0\n                  ? html`<div class=\"flex flex-wrap gap-2\">\n                      ${this.invites.map(\n                        (email) => html`\n                          <uip-badge variant=\"secondary\" class=\"gap-1 py-1 pr-1 pl-2.5\">\n                            ${email}\n                            <button\n                              type=\"button\"\n                              aria-label=${`Remove ${email}`}\n                              class=\"hover:bg-foreground/10 focus-visible:ring-ring min-h-6 rounded-full p-0.5 transition-colors focus-visible:ring-2 focus-visible:outline-none\"\n                              @click=${() => this.removeInvite(email)}\n                            >\n                              ${icon(X, 'x', 'size-3')}\n                            </button>\n                          </uip-badge>\n                        `,\n                      )}\n                    </div>`\n                  : html`<p class=\"text-muted-foreground text-xs\">No teammates invited yet.</p>`}\n                <button\n                  type=\"button\"\n                  class=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring min-h-6 text-xs underline-offset-4 transition-colors hover:underline focus-visible:ring-2 focus-visible:outline-none\"\n                  @click=${this.goNext}\n                >\n                  Skip for now\n                </button>\n              </div>\n            `\n          : nothing}\n        ${this.step === 3\n          ? html`\n              <div class=\"divide-y rounded-lg border\">\n                ${this.preferences.map(\n                  (pref) => html`\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\">${pref.label}</p>\n                        <p class=\"text-muted-foreground mt-0.5 text-xs\">${pref.description}</p>\n                      </div>\n                      <uip-switch\n                        aria-label=${pref.label}\n                        .checked=${pref.enabled}\n                        @checked-change=${(e: CustomEvent<boolean>) => {\n                          const enabled = e.detail\n                          this.preferences = this.preferences.map((p) =>\n                            p.id === pref.id ? { ...p, enabled } : p,\n                          )\n                        }}\n                      ></uip-switch>\n                    </div>\n                  `,\n                )}\n              </div>\n            `\n          : nothing}\n        ${this.step === 4\n          ? html`\n              <div class=\"space-y-4 py-6 text-center\">\n                <div\n                  class=\"border-success/30 bg-success/10 text-success mx-auto flex size-16 items-center justify-center rounded-full border shadow-xs\"\n                >\n                  ${icon(Check, 'check', 'size-8')}\n                </div>\n                <div>\n                  <p class=\"text-lg font-semibold\">You're all set</p>\n                  <p class=\"text-muted-foreground mx-auto mt-1 max-w-sm text-sm\">\n                    ${this.invites.length > 0\n                      ? `${this.workspaceName || 'Your workspace'} is ready and ${this.invites.length} invitation${this.invites.length === 1 ? '' : 's'} sent.`\n                      : `${this.workspaceName || 'Your workspace'} is ready — invite teammates anytime from settings.`}\n                  </p>\n                </div>\n                <uip-button variant=\"outline\" size=\"sm\" @click=${this.reset}>Set up another workspace</uip-button>\n              </div>\n            `\n          : nothing}\n\n        <div slot=\"footer\" class=\"flex w-full items-center justify-between\">\n          ${this.step > 1 && this.step < 4\n            ? html`<uip-button\n                variant=\"ghost\"\n                size=\"sm\"\n                @click=${() => {\n                  this.step -= 1\n                }}\n              >\n                ${icon(ChevronLeft, 'chevron-left')} Back\n              </uip-button>`\n            : html`<span aria-hidden=\"true\"></span>`}\n          <span class=\"text-muted-foreground text-xs\">Step ${this.step} of 4</span>\n          ${this.step < 4\n            ? html`<uip-button\n                size=\"sm\"\n                ?disabled=${this.step === 1 && this.workspaceName.trim() === ''}\n                @click=${this.goNext}\n              >\n                ${this.step === 3 ? 'Finish' : 'Continue'} ${icon(ChevronRight, 'chevron-right')}\n              </uip-button>`\n            : html`<span aria-hidden=\"true\"></span>`}\n        </div>\n      </uip-section-card>\n    `\n  }\n}\n\ncustomElements.get('uip-onboarding-wizard') || customElements.define('uip-onboarding-wizard', UipOnboardingWizard)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-onboarding-wizard': UipOnboardingWizard\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/blocks/onboarding-wizard/onboarding-wizard.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/badge.json",
    "https://uipkge.dev/r/lit/button.json",
    "https://uipkge.dev/r/lit/input.json",
    "https://uipkge.dev/r/lit/lit-core.json",
    "https://uipkge.dev/r/lit/section-card.json",
    "https://uipkge.dev/r/lit/stepper.json",
    "https://uipkge.dev/r/lit/switch.json"
  ]
}
