{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "import-wizard",
  "title": "Import Wizard",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-vue/blocks/import-wizard/ImportWizard.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport type { HTMLAttributes } from 'vue'\nimport { AlertTriangle, CheckCircle2, ChevronLeft, ChevronRight, FileSpreadsheet, Upload } from 'lucide-vue-next'\nimport { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { SectionCard } from '@/components/ui/section-card'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'\nimport { Stepper } from '@/components/ui/stepper'\n\ntype 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\nconst props = withDefaults(\n  defineProps<{\n    /** Jump straight to a step (1 Upload · 2 Map · 3 Preview · 4 Result). */\n    initialStep?: number\n    /** Show the upload step with the file already parsed. */\n    initialFileSelected?: boolean\n    /** Override the auto-mapping so demos can show unmapped required fields. */\n    initialMapping?: Record<string, TargetField>\n    class?: HTMLAttributes['class']\n  }>(),\n  {\n    initialStep: 1,\n    initialFileSelected: false,\n    initialMapping: undefined,\n  },\n)\n\nconst steps = [\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 = ['full_name', 'email_address', 'dept', 'job_title', 'start_date', 'manager'] as const\n\nconst defaultMapping: Record<string, 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<string, '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 step = ref(props.initialStep)\nconst fileSelected = ref(props.initialFileSelected || props.initialStep > 1)\nconst mapping = ref<Record<string, TargetField>>(props.initialMapping ?? { ...defaultMapping })\n\nconst mappedTargets = computed<TargetField[]>(() => Object.values(mapping.value).filter((t) => t !== 'Skip'))\nconst missingRequired = computed(() => requiredTargets.filter((target) => !mappedTargets.value.includes(target)))\n\nfunction cellInvalid(row: CsvRow, column: string): boolean {\n  if (mapping.value[column] === 'Full name') return row.full_name.trim() === ''\n  if (mapping.value[column] === 'Email') return !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(row.email_address)\n  return false\n}\n\nconst flaggedCount = computed(() => rows.filter((row) => columns.some((col) => cellInvalid(row, col))).length)\nconst importedCount = 245\n\nfunction onStepperInput(value: number) {\n  // Free backward navigation; forward goes through the footer button only.\n  if (value < step.value && !(step.value === 4)) step.value = value\n}\n\nfunction goNext() {\n  if (step.value === 1 && !fileSelected.value) fileSelected.value = true\n  if (step.value === 2 && missingRequired.value.length > 0) return\n  if (step.value < 4) step.value += 1\n}\n\nfunction reset() {\n  step.value = 1\n  fileSelected.value = false\n  mapping.value = { ...defaultMapping }\n}\n</script>\n\n<template>\n  <SectionCard\n    data-slot=\"import-wizard\"\n    title=\"Import employees\"\n    description=\"Bring your people data in as CSV — map, review, then commit.\"\n    :class=\"props.class\"\n  >\n    <Stepper :steps=\"steps\" :model-value=\"step\" class=\"mb-6\" @update:model-value=\"onStepperInput\" />\n\n    <!-- Step 1 · Upload -->\n    <div v-if=\"step === 1\" class=\"space-y-4\">\n      <button\n        v-if=\"!fileSelected\"\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 = true\"\n      >\n        <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      <div v-else 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          <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        <Badge wrap class=\"\" variant=\"success\">Parsed</Badge>\n      </div>\n    </div>\n\n    <!-- Step 2 · Map fields -->\n    <div v-else-if=\"step === 2\" class=\"space-y-4\">\n      <Alert v-if=\"missingRequired.length > 0\" class=\"border-warning/40 bg-warning/5\">\n        <AlertTriangle class=\"text-warning size-4\" aria-hidden=\"true\" />\n        <AlertTitle>Required fields unmapped</AlertTitle>\n        <AlertDescription>Map {{ missingRequired.join(' and ') }} before continuing.</AlertDescription>\n      </Alert>\n      <ul class=\"divide-y rounded-lg border\">\n        <li v-for=\"column in columns\" :key=\"column\" 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          <ChevronRight class=\"text-muted-foreground size-4 shrink-0\" aria-hidden=\"true\" />\n          <Select v-model=\"mapping[column]\">\n            <SelectTrigger class=\"flex-1\" size=\"sm\">\n              <SelectValue />\n            </SelectTrigger>\n            <SelectContent>\n              <SelectItem v-for=\"option in targetOptions\" :key=\"option\" :value=\"option\">{{ option }}</SelectItem>\n            </SelectContent>\n          </Select>\n          <Badge\n            wrap\n            class=\"\"\n            v-if=\"mapping[column] !== 'Skip'\"\n            :variant=\"confidence[column] === 'high' ? 'success' : 'info'\"\n          >\n            {{ confidence[column] === 'high' ? 'Auto · high' : 'Auto · medium' }}\n          </Badge>\n        </li>\n      </ul>\n    </div>\n\n    <!-- Step 3 · Preview -->\n    <div v-else-if=\"step === 3\" 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              <th v-for=\"column in columns\" :key=\"column\" class=\"px-3 py-2 text-left font-medium\">\n                {{ mapping[column] === 'Skip' ? '—' : mapping[column] }}\n              </th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr v-for=\"(row, i) in rows.slice(0, 5)\" :key=\"i\" class=\"border-b last:border-b-0\">\n              <td\n                v-for=\"column in columns\"\n                :key=\"column\"\n                class=\"px-3 py-2\"\n                :class=\"cellInvalid(row, column) ? 'bg-destructive/10 text-destructive font-medium' : ''\"\n                :title=\"cellInvalid(row, column) ? 'This value will be rejected' : undefined\"\n              >\n                {{ mapping[column] === 'Skip' ? '·' : row[column] }}\n              </td>\n            </tr>\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    <!-- Step 4 · Result -->\n    <div v-else class=\"space-y-4 py-4 text-center\">\n      <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 variant=\"outline\" size=\"sm\" @click=\"reset\">Import another file</Button>\n    </div>\n\n    <template #footer>\n      <div class=\"flex items-center justify-between\">\n        <Button v-if=\"step > 1 && step < 4\" variant=\"ghost\" size=\"sm\" @click=\"step -= 1\">\n          <ChevronLeft aria-hidden=\"true\" />\n          Back\n        </Button>\n        <span v-else></span>\n        <Button v-if=\"step < 4\" size=\"sm\" :disabled=\"step === 2 && missingRequired.length > 0\" @click=\"goNext\">\n          {{ step === 1 ? 'Continue' : step === 2 ? 'Review' : 'Import' }}\n          <ChevronRight aria-hidden=\"true\" />\n        </Button>\n      </div>\n    </template>\n  </SectionCard>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/ImportWizard.vue"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/alert.json",
    "https://uipkge.dev/r/vue/badge.json",
    "https://uipkge.dev/r/vue/button.json",
    "https://uipkge.dev/r/vue/section-card.json",
    "https://uipkge.dev/r/vue/select.json",
    "https://uipkge.dev/r/vue/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"
  ]
}