{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "kyc-verification",
  "type": "registry:component",
  "title": "KYC Verification",
  "description": "Identity, document, and selfie verification steps.",
  "author": "Stepperize",
  "dependencies": [
    "@stepperize/react",
    "lucide-react",
    "zod"
  ],
  "registryDependencies": [
    "alert",
    "button",
    "checkbox",
    "input",
    "label"
  ],
  "categories": [
    "auth"
  ],
  "meta": {
    "capabilities": [
      "navigation"
    ],
    "level": "intermediate",
    "tags": [
      "kyc",
      "identity",
      "verification",
      "documents",
      "compliance"
    ]
  },
  "files": [
    {
      "path": "components/stepperize/kyc-verification.tsx",
      "type": "registry:component",
      "target": "components/stepperize/kyc-verification.tsx",
      "content": "\"use client\";\n\nimport { defineStepper } from \"@stepperize/react\";\nimport { Camera, FileCheck, IdCard, UserRound } from \"lucide-react\";\nimport type { ComponentType } from \"react\";\nimport { useState } from \"react\";\nimport { z } from \"zod\";\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\n\n// The identity step owns data, so it carries a schema. The document and selfie\n// steps gate on a custom acknowledgement instead — both run in one guard.\nconst identitySchema = z.object({\n\tlegalName: z.string().min(2, \"Enter your legal name\"),\n\tdob: z.string().regex(/^\\d{4}-\\d{2}-\\d{2}$/, \"Use YYYY-MM-DD\"),\n});\n\nconst kyc = defineStepper(\n\t[\n\t\t{\n\t\t\tid: \"identity\",\n\t\t\ttitle: \"Identity\",\n\t\t\ticon: UserRound,\n\t\t\tschema: identitySchema,\n\t\t},\n\t\t{ id: \"document\", title: \"Document\", icon: IdCard },\n\t\t{ id: \"selfie\", title: \"Selfie\", icon: Camera },\n\t\t{ id: \"review\", title: \"Review\", icon: FileCheck },\n\t] as const,\n\t{ defaultData: { identity: { legalName: \"\", dob: \"\" } } },\n);\n\nconst { Stepper } = kyc;\n\nconst icons: Record<\n\tstring,\n\tComponentType<{ className?: string }>\n> = Object.fromEntries(\n\t[UserRound, IdCard, Camera, FileCheck].map((Icon, i) => [\n\t\t[\"identity\", \"document\", \"selfie\", \"review\"][i],\n\t\tIcon,\n\t]),\n);\n\ntype Errors = Record<string, string>;\n\nfunction toErrors(\n\tissues: ReadonlyArray<{ message: string; path?: ReadonlyArray<unknown> }>,\n): Errors {\n\tconst out: Errors = {};\n\tfor (const issue of issues) {\n\t\tconst seg = issue.path?.[0];\n\t\tconst key =\n\t\t\ttypeof seg === \"object\" && seg !== null\n\t\t\t\t? String((seg as { key: PropertyKey }).key)\n\t\t\t\t: String(seg ?? \"_\");\n\t\tout[key] ??= issue.message;\n\t}\n\treturn out;\n}\n\nexport function KycVerificationBlock() {\n\tconst [errors, setErrors] = useState<Errors>({});\n\tconst [submitted, setSubmitted] = useState(false);\n\n\treturn (\n\t\t<Stepper.Root\n\t\t\tlinear\n\t\t\tclassName=\"w-full max-w-lg rounded-xl border bg-background p-6 shadow-sm\"\n\t\t\tbeforeStepChange={async ({ direction, from, validate, data }) => {\n\t\t\t\tif (direction !== \"next\") {\n\t\t\t\t\tsetErrors({});\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\t// Schema-backed steps validate their data…\n\t\t\t\tconst result = await validate();\n\t\t\t\tif (!result.success) {\n\t\t\t\t\tsetErrors(toErrors(result.issues));\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\t// …and the upload steps require an explicit acknowledgement.\n\t\t\t\tif (from.id === \"document\" || from.id === \"selfie\") {\n\t\t\t\t\tconst ack = (data[from.id] as { confirmed?: boolean } | undefined)\n\t\t\t\t\t\t?.confirmed;\n\t\t\t\t\tif (!ack) {\n\t\t\t\t\t\tsetErrors({ _: \"Confirm the upload before continuing.\" });\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tsetErrors({});\n\t\t\t\treturn true;\n\t\t\t}}\n\t\t>\n\t\t\t{({ stepper }) => (\n\t\t\t\t<>\n\t\t\t\t\t<Stepper.List className=\"flex items-center justify-between\">\n\t\t\t\t\t\t<Stepper.Items>\n\t\t\t\t\t\t\t{(step, index) => {\n\t\t\t\t\t\t\t\tconst Icon = icons[step.id];\n\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\tkey={step.id}\n\t\t\t\t\t\t\t\t\t\tclassName=\"flex flex-1 flex-col items-center last:flex-none\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<div className=\"flex w-full items-center\">\n\t\t\t\t\t\t\t\t\t\t\t<Stepper.Item\n\t\t\t\t\t\t\t\t\t\t\t\tstep={step.id}\n\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"flex flex-col items-center\"\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t<Stepper.Trigger className=\"disabled:cursor-not-allowed\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Stepper.Indicator className=\"grid size-11 place-items-center rounded-xl border transition-colors data-[status=active]:border-primary data-[status=active]:bg-primary data-[status=active]:text-primary-foreground data-[status=previous]:border-primary data-[status=previous]:bg-primary/10 data-[status=previous]:text-primary data-[status=upcoming]:border-border data-[status=upcoming]:text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Icon className=\"size-5\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Stepper.Indicator>\n\t\t\t\t\t\t\t\t\t\t\t\t</Stepper.Trigger>\n\t\t\t\t\t\t\t\t\t\t\t\t<Stepper.Title className=\"mt-2 text-xs font-medium\" />\n\t\t\t\t\t\t\t\t\t\t\t</Stepper.Item>\n\t\t\t\t\t\t\t\t\t\t\t{index < stepper.count - 1 && (\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"-mt-6 mx-2 h-0.5 flex-1 rounded bg-border\" />\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t</Stepper.Items>\n\t\t\t\t\t</Stepper.List>\n\n\t\t\t\t\t<div className=\"mt-6 min-h-32 rounded-lg border bg-muted/30 p-4 text-sm\">\n\t\t\t\t\t\t<Stepper.Content step=\"identity\">\n\t\t\t\t\t\t\t<IdentityFields errors={errors} />\n\t\t\t\t\t\t</Stepper.Content>\n\t\t\t\t\t\t<Stepper.Content step=\"document\">\n\t\t\t\t\t\t\t<UploadAck\n\t\t\t\t\t\t\t\tstepId=\"document\"\n\t\t\t\t\t\t\t\ttitle=\"Upload your ID\"\n\t\t\t\t\t\t\t\thint=\"A passport or driver's license works best.\"\n\t\t\t\t\t\t\t\tlabel=\"I've uploaded a valid photo ID\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</Stepper.Content>\n\t\t\t\t\t\t<Stepper.Content step=\"selfie\">\n\t\t\t\t\t\t\t<UploadAck\n\t\t\t\t\t\t\t\tstepId=\"selfie\"\n\t\t\t\t\t\t\t\ttitle=\"Take a selfie\"\n\t\t\t\t\t\t\t\thint=\"We match your face against your document.\"\n\t\t\t\t\t\t\t\tlabel=\"I've captured a clear selfie\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</Stepper.Content>\n\t\t\t\t\t\t<Stepper.Content step=\"review\">\n\t\t\t\t\t\t\t<p className=\"font-medium\">\n\t\t\t\t\t\t\t\t{submitted ? \"Verification submitted\" : \"Review & submit\"}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<p className=\"mt-1 text-muted-foreground\">\n\t\t\t\t\t\t\t\t{submitted\n\t\t\t\t\t\t\t\t\t? \"We'll notify you when the review is complete.\"\n\t\t\t\t\t\t\t\t\t: \"Confirm everything looks right before sending.\"}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</Stepper.Content>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t{errors._ && (\n\t\t\t\t\t\t<Alert variant=\"destructive\" className=\"mt-3\">\n\t\t\t\t\t\t\t<AlertDescription>{errors._}</AlertDescription>\n\t\t\t\t\t\t</Alert>\n\t\t\t\t\t)}\n\n\t\t\t\t\t<Stepper.Actions className=\"mt-6 flex justify-between\">\n\t\t\t\t\t\t<Stepper.Prev className={buttonVariants({ variant: \"outline\" })}>\n\t\t\t\t\t\t\tBack\n\t\t\t\t\t\t</Stepper.Prev>\n\t\t\t\t\t\t{submitted ? (\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\t\tsetSubmitted(false);\n\t\t\t\t\t\t\t\t\tsetErrors({});\n\t\t\t\t\t\t\t\t\tstepper.data.reset();\n\t\t\t\t\t\t\t\t\tstepper.reset();\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tclassName={buttonVariants()}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tStart over\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t) : stepper.isLast ? (\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tonClick={() => setSubmitted(true)}\n\t\t\t\t\t\t\t\tclassName={buttonVariants()}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tSubmit verification\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<Stepper.Next className={buttonVariants()}>Next</Stepper.Next>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</Stepper.Actions>\n\t\t\t\t</>\n\t\t\t)}\n\t\t</Stepper.Root>\n\t);\n}\n\nfunction IdentityFields({ errors }: { errors: Errors }) {\n\tconst stepper = kyc.useStepper();\n\tconst identity = stepper.data.get(\"identity\") ?? { legalName: \"\", dob: \"\" };\n\tconst set = (patch: Partial<typeof identity>) =>\n\t\tstepper.data.set(\"identity\", { ...identity, ...patch });\n\n\treturn (\n\t\t<div className=\"space-y-3\">\n\t\t\t<p className=\"font-medium\">Personal information</p>\n\t\t\t<Field\n\t\t\t\tlabel=\"Legal name\"\n\t\t\t\tplaceholder=\"Ada Lovelace\"\n\t\t\t\tvalue={identity.legalName}\n\t\t\t\terror={errors.legalName}\n\t\t\t\tonChange={(event) => set({ legalName: event.target.value })}\n\t\t\t/>\n\t\t\t<Field\n\t\t\t\tlabel=\"Date of birth\"\n\t\t\t\tplaceholder=\"1990-12-10\"\n\t\t\t\tvalue={identity.dob}\n\t\t\t\terror={errors.dob}\n\t\t\t\tonChange={(event) => set({ dob: event.target.value })}\n\t\t\t/>\n\t\t</div>\n\t);\n}\n\nfunction UploadAck({\n\tstepId,\n\ttitle,\n\thint,\n\tlabel,\n}: {\n\tstepId: \"document\" | \"selfie\";\n\ttitle: string;\n\thint: string;\n\tlabel: string;\n}) {\n\tconst stepper = kyc.useStepper();\n\tconst confirmed =\n\t\t(stepper.data.get(stepId) as { confirmed?: boolean } | undefined)\n\t\t\t?.confirmed ?? false;\n\n\treturn (\n\t\t<div className=\"space-y-3\">\n\t\t\t<p className=\"font-medium\">{title}</p>\n\t\t\t<p className=\"text-muted-foreground\">{hint}</p>\n\t\t\t<Label className=\"flex items-center gap-2 text-sm font-normal\">\n\t\t\t\t<Checkbox\n\t\t\t\t\tchecked={confirmed}\n\t\t\t\t\tonCheckedChange={(checked) =>\n\t\t\t\t\t\tstepper.data.set(stepId, { confirmed: checked === true })\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t\t{label}\n\t\t\t</Label>\n\t\t</div>\n\t);\n}\n\nfunction Field({\n\tlabel,\n\terror,\n\t...props\n}: { label: string; error?: string } & React.ComponentProps<typeof Input>) {\n\treturn (\n\t\t<div className=\"space-y-1.5\">\n\t\t\t<Label>{label}</Label>\n\t\t\t<Input aria-invalid={error ? true : undefined} {...props} />\n\t\t\t{error && <p className=\"text-xs text-destructive\">{error}</p>}\n\t\t</div>\n\t);\n}\n\nexport default KycVerificationBlock;\n"
    }
  ]
}
