{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "import-wizard",
  "title": "Import Wizard",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/import-wizard/import-wizard.component.ts",
      "content": "// CSV import flow in a SectionCard: four-step state machine (Upload → Map fields →\n// Preview → Result) with an interactive Stepper, auto-mapped column targets carrying\n// confidence badges, required-field validation that gates the footer action, flagged-cell\n// highlighting in the preview table, and a success/partial result screen. Data is stubbed\n// post-parse — wire the file reader of your choice. Port of the React block 1:1.\nimport { ChangeDetectionStrategy, Component, Input, OnInit, computed, signal } from '@angular/core'\nimport { CheckCircle2, ChevronLeft, ChevronRight, FileSpreadsheet, LucideAngularModule, Upload } from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiAlertComponent, UiAlertDescriptionComponent, UiAlertTitleComponent } from '@/ui/alert/alert.component'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport { UiSectionCardComponent } from '@/ui/section-card/section-card.component'\nimport {\n  UiSelectComponent,\n  UiSelectContentComponent,\n  UiSelectItemComponent,\n  UiSelectTriggerComponent,\n  UiSelectValueComponent,\n} from '@/ui/select/select.component'\nimport { UiStepperComponent, type StepperStep } from '@/ui/stepper/stepper.component'\n\nexport type TargetField = 'Full name' | 'Email' | 'Department' | 'Job title' | 'Start date' | 'Skip'\n\ninterface CsvRow {\n  full_name: string\n  email_address: string\n  dept: string\n  job_title: string\n  start_date: string\n  manager: string\n}\n\ntype CsvColumn = keyof CsvRow\n\nconst steps: StepperStep[] = [\n  { id: 1, title: 'Upload' },\n  { id: 2, title: 'Map fields' },\n  { id: 3, title: 'Preview' },\n  { id: 4, title: 'Result' },\n]\n\nconst columns: CsvColumn[] = ['full_name', 'email_address', 'dept', 'job_title', 'start_date', 'manager']\n\nconst defaultMapping: Record<CsvColumn, TargetField> = {\n  full_name: 'Full name',\n  email_address: 'Email',\n  dept: 'Department',\n  job_title: 'Job title',\n  start_date: 'Start date',\n  manager: 'Skip',\n}\n\nconst confidence: Record<CsvColumn, 'high' | 'medium'> = {\n  full_name: 'high',\n  email_address: 'high',\n  dept: 'medium',\n  job_title: 'high',\n  start_date: 'medium',\n  manager: 'high',\n}\n\nconst targetOptions: TargetField[] = ['Full name', 'Email', 'Department', 'Job title', 'Start date', 'Skip']\nconst requiredTargets: TargetField[] = ['Full name', 'Email']\n\nconst rows: CsvRow[] = [\n  {\n    full_name: 'Amara Osei',\n    email_address: 'amara@acme.com',\n    dept: 'Engineering',\n    job_title: 'Backend engineer',\n    start_date: '2026-09-01',\n    manager: 'j.weber@acme.com',\n  },\n  {\n    full_name: 'Marcus Lee',\n    email_address: 'marcus.lee@acme.com',\n    dept: 'Design',\n    job_title: 'Product designer',\n    start_date: '2026-09-01',\n    manager: 'a.osei@acme.com',\n  },\n  {\n    full_name: '',\n    email_address: 'no-name@acme.com',\n    dept: 'Sales',\n    job_title: 'Account exec',\n    start_date: '2026-09-08',\n    manager: 'p.nair@acme.com',\n  },\n  {\n    full_name: 'Priya Nair',\n    email_address: 'not-an-email',\n    dept: 'People',\n    job_title: 'Recruiter',\n    start_date: '2026-09-15',\n    manager: 'a.osei@acme.com',\n  },\n  {\n    full_name: 'Jonas Weber',\n    email_address: 'jonas@acme.com',\n    dept: 'Engineering',\n    job_title: 'Eng manager',\n    start_date: '2026-08-24',\n    manager: '',\n  },\n]\n\nconst EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-import-wizard, [ui-import-wizard]',\n  standalone: true,\n  // React renders the SectionCard itself: the host stays out of layout and `class` goes to the card.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [\n    LucideAngularModule,\n    UiAlertComponent,\n    UiAlertDescriptionComponent,\n    UiAlertTitleComponent,\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiSectionCardComponent,\n    UiSelectComponent,\n    UiSelectContentComponent,\n    UiSelectItemComponent,\n    UiSelectTriggerComponent,\n    UiSelectValueComponent,\n    UiStepperComponent,\n  ],\n  template: `\n    <ui-section-card\n      data-slot=\"import-wizard\"\n      [title]=\"'Import employees'\"\n      [description]=\"'Bring your people data in as CSV — map, review, then commit.'\"\n      [class]=\"className\"\n    >\n      <ui-stepper [steps]=\"steps\" [value]=\"step()\" (valueChange)=\"onStepperInput($event)\" class=\"mb-6\" />\n\n      @if (step() === 1) {\n        <div class=\"space-y-4\">\n          @if (!fileSelected()) {\n            <button\n              type=\"button\"\n              class=\"border-border hover:border-primary/50 hover:bg-accent/50 focus-visible:ring-ring flex w-full cursor-pointer flex-col items-center gap-2 rounded-lg border border-dashed px-6 py-10 transition-colors focus-visible:ring-2 focus-visible:outline-none\"\n              (click)=\"fileSelected.set(true)\"\n            >\n              <lucide-icon [img]=\"Upload\" class=\"text-muted-foreground size-6\" aria-hidden=\"true\" />\n              <span class=\"text-sm font-medium\">Drop your CSV here or browse</span>\n              <span class=\"text-muted-foreground text-xs\">Up to 10 MB · comma or semicolon separated</span>\n            </button>\n          } @else {\n            <div class=\"border-border flex flex-wrap items-center gap-3 rounded-lg border p-4\">\n              <span\n                class=\"bg-muted text-muted-foreground flex size-9 items-center justify-center rounded-md\"\n                aria-hidden=\"true\"\n              >\n                <lucide-icon [img]=\"FileSpreadsheet\" class=\"size-4\" />\n              </span>\n              <div class=\"min-w-0 flex-1\">\n                <p class=\"truncate text-sm font-medium\">employees.csv</p>\n                <p class=\"text-muted-foreground text-xs\">248 rows · 6 columns · 38 KB</p>\n              </div>\n              <span ui-badge variant=\"success\">Parsed</span>\n            </div>\n          }\n        </div>\n      }\n\n      @if (step() === 2) {\n        <div class=\"space-y-4\">\n          @if (missingRequired().length > 0) {\n            <ui-alert class=\"border-warning/40 bg-warning/5\">\n              <ui-alert-title>Required fields unmapped</ui-alert-title>\n              <ui-alert-description>Map {{ missingRequired().join(' and ') }} before continuing.</ui-alert-description>\n            </ui-alert>\n          }\n          <ul class=\"divide-y rounded-lg border\">\n            @for (column of columns; track column) {\n              <li class=\"flex flex-wrap items-center gap-3 p-3\">\n                <code class=\"w-32 shrink-0 truncate font-mono text-xs\">{{ column }}</code>\n                <lucide-icon [img]=\"ChevronRight\" class=\"text-muted-foreground size-4 shrink-0\" aria-hidden=\"true\" />\n                <ui-select [value]=\"mapping()[column]\" (valueChange)=\"setMapping(column, $event)\">\n                  <button ui-select-trigger size=\"sm\" class=\"flex-1\">\n                    <ui-select-value />\n                  </button>\n                  <ui-select-content>\n                    @for (option of targetOptions; track option) {\n                      <ui-select-item [value]=\"option\">{{ option }}</ui-select-item>\n                    }\n                  </ui-select-content>\n                </ui-select>\n                @if (mapping()[column] !== 'Skip') {\n                  <span ui-badge [variant]=\"confidenceFor(column) === 'high' ? 'success' : 'info'\">\n                    {{ confidenceFor(column) === 'high' ? 'Auto · high' : 'Auto · medium' }}\n                  </span>\n                }\n              </li>\n            }\n          </ul>\n        </div>\n      }\n\n      @if (step() === 3) {\n        <div class=\"space-y-3\">\n          <div class=\"overflow-x-auto rounded-lg border\">\n            <table class=\"w-full max-w-[640px] min-w-full text-sm\">\n              <thead>\n                <tr class=\"bg-muted/50 border-b\">\n                  @for (column of columns; track column) {\n                    <th class=\"px-3 py-2 text-left font-medium\">\n                      {{ mapping()[column] === 'Skip' ? '—' : mapping()[column] }}\n                    </th>\n                  }\n                </tr>\n              </thead>\n              <tbody>\n                @for (row of previewRows; track $index) {\n                  <tr class=\"border-b last:border-b-0\">\n                    @for (column of columns; track column) {\n                      <td\n                        [class]=\"cellClass(row, column)\"\n                        [attr.title]=\"cellInvalid(row, column) ? 'This value will be rejected' : null\"\n                      >\n                        {{ mapping()[column] === 'Skip' ? '·' : row[column] }}\n                      </td>\n                    }\n                  </tr>\n                }\n              </tbody>\n            </table>\n          </div>\n          <p class=\"text-muted-foreground text-xs\">\n            Showing first 5 of 248 rows ·\n            <span [class]=\"flaggedCount() > 0 ? 'text-warning font-medium' : 'text-success font-medium'\">\n              {{ importedCount }} valid · {{ flaggedCount() }} flagged\n            </span>\n          </p>\n        </div>\n      }\n\n      @if (step() === 4) {\n        <div class=\"space-y-4 py-4 text-center\">\n          <lucide-icon [img]=\"CheckCircle2\" class=\"text-success mx-auto size-10\" aria-hidden=\"true\" />\n          <div>\n            <p class=\"text-lg font-semibold\">{{ importedCount }} records imported</p>\n            <p class=\"text-muted-foreground mt-1 text-sm\">\n              {{ flaggedCount() }} rows were skipped — fix them and re-import anytime.\n            </p>\n          </div>\n          <button ui-button variant=\"outline\" size=\"sm\" (click)=\"reset()\">Import another file</button>\n        </div>\n      }\n\n      <div slot=\"footer\" class=\"flex w-full items-center justify-between\">\n        @if (step() > 1 && step() < 4) {\n          <button ui-button variant=\"ghost\" size=\"sm\" class=\"px-2.5\" (click)=\"step.set(step() - 1)\">\n            <lucide-icon [img]=\"ChevronLeft\" aria-hidden=\"true\" />\n            Back\n          </button>\n        } @else {\n          <span></span>\n        }\n        @if (step() < 4) {\n          <button\n            ui-button\n            size=\"sm\"\n            class=\"px-2.5\"\n            [disabled]=\"step() === 2 && missingRequired().length > 0\"\n            (click)=\"goNext()\"\n          >\n            {{ step() === 1 ? 'Continue' : step() === 2 ? 'Review' : 'Import' }}\n            <lucide-icon [img]=\"ChevronRight\" aria-hidden=\"true\" />\n          </button>\n        }\n      </div>\n    </ui-section-card>\n  `,\n})\nexport class UiImportWizardComponent implements OnInit {\n  protected readonly CheckCircle2 = CheckCircle2\n  protected readonly ChevronLeft = ChevronLeft\n  protected readonly ChevronRight = ChevronRight\n  protected readonly FileSpreadsheet = FileSpreadsheet\n  protected readonly Upload = Upload\n\n  protected readonly steps = steps\n  protected readonly columns = columns\n  protected readonly targetOptions = targetOptions\n  protected readonly previewRows = rows.slice(0, 5)\n  protected readonly importedCount = 245\n\n  /** Jump straight to a step (1 Upload · 2 Map · 3 Preview · 4 Result). */\n  @Input() initialStep = 1\n  /** Show the upload step with the file already parsed. */\n  @Input() initialFileSelected = false\n  /** Override the auto-mapping so demos can show unmapped required fields. */\n  @Input() initialMapping?: Record<string, TargetField>\n  @Input('class') className?: string\n\n  readonly step = signal(1)\n  readonly fileSelected = signal(false)\n  readonly mapping = signal<Record<CsvColumn, TargetField>>({ ...defaultMapping })\n\n  constructor() {}\n\n  ngOnInit(): void {\n    this.step.set(this.initialStep)\n    this.fileSelected.set(this.initialFileSelected || this.initialStep > 1)\n    if (this.initialMapping) this.mapping.set({ ...defaultMapping, ...this.initialMapping })\n  }\n\n  readonly mappedTargets = computed<TargetField[]>(() =>\n    (Object.values(this.mapping()) as TargetField[]).filter((t) => t !== 'Skip'),\n  )\n\n  readonly missingRequired = computed(() => requiredTargets.filter((target) => !this.mappedTargets().includes(target)))\n\n  readonly flaggedCount = computed(() => rows.filter((row) => columns.some((col) => this.cellInvalid(row, col))).length)\n\n  confidenceFor(column: CsvColumn): 'high' | 'medium' {\n    return confidence[column]\n  }\n\n  cellInvalid(row: CsvRow, column: string): boolean {\n    if (this.mapping()[column as CsvColumn] === 'Full name') return row.full_name.trim() === ''\n    if (this.mapping()[column as CsvColumn] === 'Email') return !EMAIL_RE.test(row.email_address)\n    return false\n  }\n\n  cellClass(row: CsvRow, column: CsvColumn): string {\n    return cn('px-3 py-2', this.cellInvalid(row, column) && 'bg-destructive/10 text-destructive font-medium')\n  }\n\n  setMapping(column: CsvColumn, value: string): void {\n    this.mapping.update((prev) => ({ ...prev, [column]: value as TargetField }))\n  }\n\n  onStepperInput(value: number): void {\n    // Free backward navigation; forward goes through the footer button only.\n    if (value < this.step() && this.step() !== 4) this.step.set(value)\n  }\n\n  goNext(): void {\n    if (this.step() === 1 && !this.fileSelected()) this.fileSelected.set(true)\n    if (this.step() === 2 && this.missingRequired().length > 0) return\n    if (this.step() < 4) this.step.set(this.step() + 1)\n  }\n\n  reset(): void {\n    this.step.set(1)\n    this.fileSelected.set(false)\n    this.mapping.set({ ...defaultMapping })\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/import-wizard/import-wizard.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/import-wizard/index.ts",
      "content": "export { UiImportWizardComponent, type TargetField } from './import-wizard.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/import-wizard/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/alert.json",
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/section-card.json",
    "https://uipkge.dev/r/angular/select.json",
    "https://uipkge.dev/r/angular/stepper.json"
  ],
  "description": "CSV import flow in a SectionCard: four-step state machine (Upload → Map fields → Preview → Result) with an interactive Stepper, auto-mapped column targets carrying confidence badges, required-field validation that gates the footer action, flagged-cell highlighting in the preview table, and a success/partial result screen. Data is stubbed post-parse — wire the file reader of your choice.",
  "categories": [
    "devops",
    "dashboard",
    "data"
  ]
}