{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "team-invites",
  "type": "registry:component",
  "title": "Team Invitations",
  "description": "Collect invites into draft flow data, then review and send them.",
  "author": "Stepperize",
  "dependencies": [
    "@stepperize/react",
    "lucide-react"
  ],
  "registryDependencies": [
    "badge",
    "button",
    "input",
    "select"
  ],
  "categories": [
    "onboarding"
  ],
  "meta": {
    "capabilities": [
      "persistence"
    ],
    "level": "intermediate",
    "tags": [
      "invites",
      "team",
      "draft data",
      "review"
    ]
  },
  "files": [
    {
      "path": "components/stepperize/team-invites.tsx",
      "type": "registry:component",
      "target": "components/stepperize/team-invites.tsx",
      "content": "\"use client\";\n\nimport { defineStepper } from \"@stepperize/react\";\nimport { Check, Plus, Send, Trash2, Users } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n\tSelect,\n\tSelectContent,\n\tSelectItem,\n\tSelectTrigger,\n\tSelectValue,\n} from \"@/components/ui/select\";\n\ntype Invite = { email: string; role: \"Member\" | \"Admin\" };\n\nconst team = defineStepper([\n\t{ id: \"invite\", title: \"Invite your team\" },\n\t{ id: \"review\", title: \"Review invites\" },\n\t{ id: \"sent\", title: \"Invites sent\" },\n]);\n\nconst { Stepper } = team;\n\n/**\n * Draft values and review: invites are stored in `stepper.data` as they're\n * added, then the review step reads them back before sending. Inner components\n * call `team.useStepper()` to read the same instance the Root provides.\n */\nexport function TeamInvitesBlock() {\n\treturn (\n\t\t<Stepper.Root\n\t\t\tlinear\n\t\t\tclassName=\"w-full max-w-sm rounded-xl border bg-background p-5 shadow-sm\"\n\t\t>\n\t\t\t{() => (\n\t\t\t\t<>\n\t\t\t\t\t<Header />\n\t\t\t\t\t<InviteStep />\n\t\t\t\t\t<ReviewStep />\n\t\t\t\t\t<SentStep />\n\t\t\t\t</>\n\t\t\t)}\n\t\t</Stepper.Root>\n\t);\n}\n\nfunction useInvites() {\n\tconst stepper = team.useStepper();\n\tconst invites = (stepper.data.get(\"invite\") as Invite[] | undefined) ?? [];\n\treturn { stepper, invites };\n}\n\nfunction Header() {\n\tconst { stepper, invites } = useInvites();\n\treturn (\n\t\t<div className=\"mb-4 flex items-center gap-2.5\">\n\t\t\t<span className=\"grid size-8 place-items-center rounded-lg bg-primary/10 text-primary\">\n\t\t\t\t<Users className=\"size-4\" />\n\t\t\t</span>\n\t\t\t<div>\n\t\t\t\t<p className=\"text-sm font-semibold\">{stepper.current.title}</p>\n\t\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\t\t{invites.length} {invites.length === 1 ? \"invite\" : \"invites\"} so far\n\t\t\t\t</p>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n\nfunction InviteStep() {\n\tconst { stepper, invites } = useInvites();\n\tconst [email, setEmail] = useState(\"\");\n\tconst [role, setRole] = useState<Invite[\"role\"]>(\"Member\");\n\n\tconst add = () => {\n\t\tif (!email.includes(\"@\")) return;\n\t\tstepper.data.set(\"invite\", [...invites, { email, role }]);\n\t\tsetEmail(\"\");\n\t};\n\tconst remove = (index: number) =>\n\t\tstepper.data.set(\n\t\t\t\"invite\",\n\t\t\tinvites.filter((_, i) => i !== index),\n\t\t);\n\n\treturn (\n\t\t<Stepper.Content step=\"invite\" className=\"space-y-3\">\n\t\t\t<div className=\"flex gap-2\">\n\t\t\t\t<Input\n\t\t\t\t\tvalue={email}\n\t\t\t\t\tonChange={(e) => setEmail(e.target.value)}\n\t\t\t\t\tonKeyDown={(e) => e.key === \"Enter\" && add()}\n\t\t\t\t\tplaceholder=\"name@company.com\"\n\t\t\t\t\tclassName=\"min-w-0 flex-1\"\n\t\t\t\t/>\n\t\t\t\t<Select\n\t\t\t\t\tvalue={role}\n\t\t\t\t\tonValueChange={(value) => setRole(value as Invite[\"role\"])}\n\t\t\t\t>\n\t\t\t\t\t<SelectTrigger>\n\t\t\t\t\t\t<SelectValue />\n\t\t\t\t\t</SelectTrigger>\n\t\t\t\t\t<SelectContent>\n\t\t\t\t\t\t<SelectItem value=\"Member\">Member</SelectItem>\n\t\t\t\t\t\t<SelectItem value=\"Admin\">Admin</SelectItem>\n\t\t\t\t\t</SelectContent>\n\t\t\t\t</Select>\n\t\t\t\t<Button size=\"icon\" onClick={add} aria-label=\"Add invite\">\n\t\t\t\t\t<Plus />\n\t\t\t\t</Button>\n\t\t\t</div>\n\n\t\t\t<ul className=\"space-y-1.5\">\n\t\t\t\t{invites.map((invite, i) => (\n\t\t\t\t\t<li\n\t\t\t\t\t\tkey={invite.email}\n\t\t\t\t\t\tclassName=\"flex items-center gap-2 rounded-lg border bg-muted/20 px-3 py-2 text-sm\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<span className=\"flex-1 truncate\">{invite.email}</span>\n\t\t\t\t\t\t<Badge variant=\"secondary\">{invite.role}</Badge>\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\tsize=\"icon-sm\"\n\t\t\t\t\t\t\tonClick={() => remove(i)}\n\t\t\t\t\t\t\taria-label={`Remove ${invite.email}`}\n\t\t\t\t\t\t\tclassName=\"text-muted-foreground hover:text-destructive\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Trash2 />\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</li>\n\t\t\t\t))}\n\t\t\t</ul>\n\n\t\t\t<Button\n\t\t\t\tclassName=\"w-full\"\n\t\t\t\tdisabled={invites.length === 0}\n\t\t\t\tonClick={() => stepper.next()}\n\t\t\t>\n\t\t\t\tReview {invites.length > 0 ? `${invites.length} ` : \"\"}invites\n\t\t\t</Button>\n\t\t</Stepper.Content>\n\t);\n}\n\nfunction ReviewStep() {\n\tconst { stepper, invites } = useInvites();\n\treturn (\n\t\t<Stepper.Content step=\"review\" className=\"space-y-3\">\n\t\t\t<p className=\"text-sm text-muted-foreground\">\n\t\t\t\tYou're inviting <b className=\"text-foreground\">{invites.length}</b>{\" \"}\n\t\t\t\t{invites.length === 1 ? \"person\" : \"people\"}.\n\t\t\t</p>\n\t\t\t<ul className=\"space-y-1.5\">\n\t\t\t\t{invites.map((invite) => (\n\t\t\t\t\t<li\n\t\t\t\t\t\tkey={invite.email}\n\t\t\t\t\t\tclassName=\"flex items-center justify-between rounded-lg border bg-muted/20 px-3 py-2 text-sm\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<span className=\"truncate\">{invite.email}</span>\n\t\t\t\t\t\t<Badge variant=\"secondary\">{invite.role}</Badge>\n\t\t\t\t\t</li>\n\t\t\t\t))}\n\t\t\t</ul>\n\t\t\t<div className=\"flex items-center justify-between gap-3\">\n\t\t\t\t<Button variant=\"ghost\" size=\"sm\" onClick={() => stepper.prev()}>\n\t\t\t\t\tEdit\n\t\t\t\t</Button>\n\t\t\t\t<Button\n\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\tstepper.setComplete(\"review\");\n\t\t\t\t\t\tstepper.next();\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<Send /> Send invites\n\t\t\t\t</Button>\n\t\t\t</div>\n\t\t</Stepper.Content>\n\t);\n}\n\nfunction SentStep() {\n\tconst { stepper, invites } = useInvites();\n\treturn (\n\t\t<Stepper.Content\n\t\t\tstep=\"sent\"\n\t\t\tclassName=\"grid place-items-center gap-2 py-4 text-center\"\n\t\t>\n\t\t\t<span className=\"grid size-12 place-items-center rounded-full bg-chart-2/15 text-chart-2\">\n\t\t\t\t<Check className=\"size-6\" />\n\t\t\t</span>\n\t\t\t<p className=\"text-sm font-medium\">{invites.length} invites sent</p>\n\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\tYour teammates will get an email to join.\n\t\t\t</p>\n\t\t\t<Button\n\t\t\t\tvariant=\"link\"\n\t\t\t\tsize=\"sm\"\n\t\t\t\tonClick={() => {\n\t\t\t\t\tstepper.data.reset();\n\t\t\t\t\tstepper.reset();\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\tInvite more\n\t\t\t</Button>\n\t\t</Stepper.Content>\n\t);\n}\n\nexport default TeamInvitesBlock;\n"
    }
  ]
}
