{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "team-invitation",
  "type": "registry:ui",
  "registryDependencies": [],
  "dependencies": ["motion", "lucide-react"],
  "devDependencies": [],
  "files": [
    {
      "path": "team-invitation.tsx",
      "content": "\"use client\";\n\nimport { ChevronDown, Clock, Mail, RotateCw, X } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  Children,\n  cloneElement,\n  createContext,\n  isValidElement,\n  type ReactElement,\n  type ReactNode,\n  type Ref,\n  useCallback,\n  useContext,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst SPRING = {\n  type: \"spring\" as const,\n  stiffness: 340,\n  damping: 32,\n};\n\nconst MORPH_SPRING = {\n  type: \"spring\" as const,\n  stiffness: 260,\n  damping: 28,\n};\n\n/** Bouncier spring so the active tab visibly stretches as it expands. */\nconst TAB_SPRING = {\n  type: \"spring\" as const,\n  stiffness: 420,\n  damping: 30,\n};\n\nexport type TeamMember = {\n  id: string;\n  name: string;\n  email: string;\n  role: string;\n  /** Avatar image URL. Falls back to initials when missing or broken. */\n  avatar?: string;\n};\n\nexport type PendingInvite = {\n  id: string;\n  email: string;\n  /** e.g. \"Sent 2d ago\". */\n  sentAt?: string;\n};\n\ntype TeamInvitationContextValue = {\n  variant: string;\n  setVariant: (variant: string) => void;\n  members: TeamMember[];\n  pending: PendingInvite[];\n  roles: string[];\n  email: string;\n  setEmail: (email: string) => void;\n  invite: () => void;\n  changeRole: (id: string, role: string) => void;\n  removeMember: (id: string) => void;\n  cancelInvite: (id: string) => void;\n  resend: (id: string) => void;\n  onClose?: () => void;\n};\n\nconst TeamInvitationContext = createContext<TeamInvitationContextValue | null>(\n  null\n);\n\nfunction useTeamInvitationContext(component: string) {\n  const context = useContext(TeamInvitationContext);\n\n  if (!context) {\n    throw new Error(`${component} must be used within <TeamInvitation>.`);\n  }\n\n  return context;\n}\n\nexport type TeamInvitationProps = {\n  children: ReactNode;\n  /** Controlled variant when the parent owns which panel is shown. */\n  variant?: string;\n  /** Initial panel for uncontrolled usage. */\n  defaultVariant?: string;\n  onVariantChange?: (variant: string) => void;\n  /** Seed member list. The block keeps a working copy for role and remove interactions. */\n  members?: TeamMember[];\n  /** Seed pending invite list. */\n  pendingInvites?: PendingInvite[];\n  /** Assignable roles shown in role dropdowns and role groups. */\n  roles?: string[];\n  onInvite?: (email: string) => void;\n  onRoleChange?: (id: string, role: string) => void;\n  onRemove?: (id: string) => void;\n  onResend?: (id: string) => void;\n  onCancelInvite?: (id: string) => void;\n  onClose?: () => void;\n  className?: string;\n};\n\nexport function TeamInvitation({\n  children,\n  variant,\n  defaultVariant = \"\",\n  onVariantChange,\n  members: seedMembers = [],\n  pendingInvites: seedPending = [],\n  roles = [],\n  onInvite,\n  onRoleChange,\n  onRemove,\n  onResend,\n  onCancelInvite,\n  onClose,\n  className,\n}: TeamInvitationProps) {\n  const [internalVariant, setInternalVariant] = useState(defaultVariant);\n  const [members, setMembers] = useState(seedMembers);\n  const [pending, setPending] = useState(seedPending);\n  const [email, setEmail] = useState(\"\");\n\n  const activeVariant = variant ?? internalVariant;\n\n  const setVariant = useCallback(\n    (next: string) => {\n      if (variant === undefined) {\n        setInternalVariant(next);\n      }\n      onVariantChange?.(next);\n    },\n    [variant, onVariantChange]\n  );\n\n  const changeRole = useCallback(\n    (id: string, role: string) => {\n      setMembers((current) =>\n        current.map((member) =>\n          member.id === id ? { ...member, role } : member\n        )\n      );\n      onRoleChange?.(id, role);\n    },\n    [onRoleChange]\n  );\n\n  const removeMember = useCallback(\n    (id: string) => {\n      setMembers((current) => current.filter((member) => member.id !== id));\n      onRemove?.(id);\n    },\n    [onRemove]\n  );\n\n  const cancelInvite = useCallback(\n    (id: string) => {\n      setPending((current) => current.filter((invite) => invite.id !== id));\n      onCancelInvite?.(id);\n    },\n    [onCancelInvite]\n  );\n\n  const resend = useCallback(\n    (id: string) => {\n      onResend?.(id);\n    },\n    [onResend]\n  );\n\n  const invite = useCallback(() => {\n    const value = email.trim();\n    if (!value) {\n      return;\n    }\n    setPending((current) => [\n      ...current,\n      { id: `invite-${Date.now()}`, email: value, sentAt: \"Just now\" },\n    ]);\n    onInvite?.(value);\n    setEmail(\"\");\n  }, [email, onInvite]);\n\n  const value = useMemo(\n    () => ({\n      variant: activeVariant,\n      setVariant,\n      members,\n      pending,\n      roles,\n      email,\n      setEmail,\n      invite,\n      changeRole,\n      removeMember,\n      cancelInvite,\n      resend,\n      onClose,\n    }),\n    [\n      activeVariant,\n      setVariant,\n      members,\n      pending,\n      roles,\n      email,\n      invite,\n      changeRole,\n      removeMember,\n      cancelInvite,\n      resend,\n      onClose,\n    ]\n  );\n\n  return (\n    <TeamInvitationContext.Provider value={value}>\n      <div className={cn(\"w-full max-w-md\", className)}>{children}</div>\n    </TeamInvitationContext.Provider>\n  );\n}\n\nexport function TeamInvitationCard({\n  children,\n  className,\n}: {\n  children: ReactNode;\n  className?: string;\n}) {\n  const reduceMotion = useReducedMotion() === true;\n\n  return (\n    <motion.section\n      animate={{ opacity: 1, y: 0, scale: 1 }}\n      className={cn(\n        \"relative overflow-visible rounded-3xl border border-foreground/8 bg-card p-5 sm:p-6\",\n        className\n      )}\n      initial={reduceMotion ? false : { opacity: 0, y: 16, scale: 0.98 }}\n      layout\n      transition={SPRING}\n    >\n      {children}\n    </motion.section>\n  );\n}\n\nexport function TeamInvitationHeader({\n  children,\n  icon,\n  className,\n}: {\n  children: ReactNode;\n  /** Leading icon rendered in a bordered tile. */\n  icon?: ReactNode;\n  className?: string;\n}) {\n  const { onClose } = useTeamInvitationContext(\"TeamInvitationHeader\");\n\n  return (\n    <div className={cn(\"flex items-start gap-4\", className)}>\n      {icon ? (\n        <span className=\"flex size-12 shrink-0 items-center justify-center rounded-xl border border-foreground/10 text-foreground [&_svg]:size-5\">\n          {icon}\n        </span>\n      ) : null}\n      <div className=\"min-w-0 flex-1\">{children}</div>\n      {onClose ? (\n        <button\n          aria-label=\"Close\"\n          className=\"rounded-lg p-1 text-foreground transition-colors hover:bg-muted\"\n          onClick={onClose}\n          type=\"button\"\n        >\n          <X className=\"size-5\" />\n        </button>\n      ) : null}\n    </div>\n  );\n}\n\nexport function TeamInvitationTitle({\n  children,\n  className,\n}: {\n  children: ReactNode;\n  className?: string;\n}) {\n  return (\n    <h2 className={cn(\"font-semibold text-foreground text-lg\", className)}>\n      {children}\n    </h2>\n  );\n}\n\nexport function TeamInvitationDescription({\n  children,\n  className,\n}: {\n  children: ReactNode;\n  className?: string;\n}) {\n  return (\n    <p className={cn(\"mt-0.5 text-[13px] text-muted-foreground\", className)}>\n      {children}\n    </p>\n  );\n}\n\nexport function TeamInvitationTabs({\n  children,\n  className,\n}: {\n  children: ReactNode;\n  className?: string;\n}) {\n  return (\n    <>\n      <div className=\"mt-5 border-foreground/8 border-t\" />\n      <div className={cn(\"mt-4 flex items-center gap-2\", className)}>\n        {children}\n      </div>\n    </>\n  );\n}\n\nexport function TeamInvitationTab({\n  value,\n  icon,\n  children,\n  className,\n}: {\n  /** Matches the value of the TeamInvitationPanel this tab reveals. */\n  value: string;\n  icon?: ReactNode;\n  children: ReactNode;\n  className?: string;\n}) {\n  const { variant, setVariant } = useTeamInvitationContext(\"TeamInvitationTab\");\n  const reduceMotion = useReducedMotion() === true;\n  const isActive = value === variant;\n\n  return (\n    <motion.button\n      aria-pressed={isActive}\n      className={cn(\n        \"relative flex h-9 shrink-0 items-center overflow-hidden rounded-full font-medium text-[13px] transition-colors duration-300\",\n        isActive\n          ? \"bg-foreground text-background\"\n          : \"bg-muted/60 text-muted-foreground hover:bg-muted hover:text-foreground\",\n        className\n      )}\n      layout\n      onClick={() => setVariant(value)}\n      transition={reduceMotion ? { duration: 0 } : TAB_SPRING}\n      type=\"button\"\n      whileTap={reduceMotion ? undefined : { scale: 0.96 }}\n    >\n      {icon ? (\n        <span className=\"flex size-9 shrink-0 items-center justify-center [&_svg]:size-4\">\n          {icon}\n        </span>\n      ) : null}\n      <AnimatePresence initial={false} mode=\"popLayout\">\n        {isActive ? (\n          <motion.span\n            animate={{ opacity: 1 }}\n            className={cn(\"whitespace-nowrap pr-3.5\", icon ? \"\" : \"pl-3.5\")}\n            exit={{ opacity: 0 }}\n            initial={reduceMotion ? false : { opacity: 0 }}\n            layout=\"position\"\n            transition={reduceMotion ? { duration: 0 } : TAB_SPRING}\n          >\n            {children}\n          </motion.span>\n        ) : null}\n      </AnimatePresence>\n      {isActive ? null : <span className=\"sr-only\">{children}</span>}\n    </motion.button>\n  );\n}\n\nexport type TeamInvitationPanelProps = {\n  /** Matches the value of the TeamInvitationTab that reveals this panel. */\n  value: string;\n  children: ReactNode;\n  className?: string;\n  /** Forwarded so AnimatePresence popLayout can measure and pop the exiting panel. */\n  ref?: Ref<HTMLDivElement>;\n};\n\n/** Lets rows namespace their layoutId per panel so exiting and entering panels never fight over the same element. */\nconst PanelValueContext = createContext(\"\");\n\nexport function TeamInvitationPanel({\n  value,\n  children,\n  className,\n  ref,\n}: TeamInvitationPanelProps) {\n  const reduceMotion = useReducedMotion() === true;\n\n  return (\n    <PanelValueContext.Provider value={value}>\n      <motion.div\n        animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n        className={cn(\"w-full\", className)}\n        exit={\n          reduceMotion\n            ? { opacity: 0 }\n            : { opacity: 0, y: -10, filter: \"blur(4px)\" }\n        }\n        initial={\n          reduceMotion\n            ? { opacity: 0 }\n            : { opacity: 0, y: 14, filter: \"blur(4px)\" }\n        }\n        ref={ref}\n        transition={MORPH_SPRING}\n      >\n        {children}\n      </motion.div>\n    </PanelValueContext.Provider>\n  );\n}\n\n/** Identity-independent marker so children traversal survives HMR module swaps. */\nTeamInvitationPanel.isTeamInvitationPanel = true;\n\nexport function TeamInvitationPanels({ children }: { children: ReactNode }) {\n  const { variant } = useTeamInvitationContext(\"TeamInvitationPanels\");\n  const reduceMotion = useReducedMotion() === true;\n  const innerRef = useRef<HTMLDivElement>(null);\n  const [height, setHeight] = useState<number | null>(null);\n\n  // Track the active panel's natural height so the container morphs\n  // smoothly between panels instead of snapping.\n  useLayoutEffect(() => {\n    const node = innerRef.current;\n    if (!node) {\n      return;\n    }\n\n    const measure = () => setHeight(node.offsetHeight);\n\n    measure();\n\n    const observer = new ResizeObserver(measure);\n    observer.observe(node);\n\n    return () => observer.disconnect();\n  }, []);\n\n  let active: ReactElement<TeamInvitationPanelProps> | null = null;\n\n  Children.forEach(children, (child) => {\n    if (!isValidElement(child)) {\n      return;\n    }\n\n    const type = child.type as { isTeamInvitationPanel?: boolean };\n\n    if (\n      (type === TeamInvitationPanel || type.isTeamInvitationPanel === true) &&\n      (child.props as TeamInvitationPanelProps).value === variant\n    ) {\n      active = child as ReactElement<TeamInvitationPanelProps>;\n    }\n  });\n\n  return (\n    <motion.div\n      animate={height === null ? undefined : { height }}\n      className=\"relative overflow-visible\"\n      initial={false}\n      transition={reduceMotion ? { duration: 0 } : MORPH_SPRING}\n    >\n      <div className=\"relative flow-root\" ref={innerRef}>\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          {active ? cloneElement(active, { key: variant }) : null}\n        </AnimatePresence>\n      </div>\n    </motion.div>\n  );\n}\n\nexport function TeamInvitationSectionLabel({\n  children,\n  className,\n}: {\n  children: ReactNode;\n  className?: string;\n}) {\n  return (\n    <h3 className={cn(\"font-medium text-[17px] text-foreground\", className)}>\n      {children}\n    </h3>\n  );\n}\n\nexport function TeamInvitationInviteField({\n  label,\n  placeholder = \"name@company.com\",\n  buttonLabel,\n  className,\n}: {\n  label?: ReactNode;\n  placeholder?: string;\n  buttonLabel: ReactNode;\n  className?: string;\n}) {\n  const { email, setEmail, invite } = useTeamInvitationContext(\n    \"TeamInvitationInviteField\"\n  );\n  const reduceMotion = useReducedMotion() === true;\n\n  return (\n    <div className={cn(\"mt-5\", className)}>\n      {label ? (\n        <TeamInvitationSectionLabel>{label}</TeamInvitationSectionLabel>\n      ) : null}\n      <div className=\"mt-3 flex gap-3\">\n        <label className=\"flex min-w-0 flex-1 items-center gap-2.5 rounded-xl border border-foreground/8 bg-muted/40 px-4 py-3 transition-colors focus-within:border-foreground/20\">\n          <Mail className=\"size-4 shrink-0 text-muted-foreground\" />\n          <input\n            className=\"w-full bg-transparent text-[14px] text-foreground outline-none placeholder:text-muted-foreground\"\n            onChange={(event) => setEmail(event.target.value)}\n            onKeyDown={(event) => {\n              if (event.key === \"Enter\") {\n                invite();\n              }\n            }}\n            placeholder={placeholder}\n            type=\"email\"\n            value={email}\n          />\n        </label>\n        <motion.button\n          className=\"shrink-0 rounded-xl bg-primary px-4 py-3 font-medium text-[14px] text-primary-foreground\"\n          onClick={invite}\n          type=\"button\"\n          whileTap={reduceMotion ? undefined : { scale: 0.96 }}\n        >\n          {buttonLabel}\n        </motion.button>\n      </div>\n    </div>\n  );\n}\n\nfunction initialsOf(name: string) {\n  return name\n    .split(\" \")\n    .map((part) => part[0])\n    .filter(Boolean)\n    .slice(0, 2)\n    .join(\"\")\n    .toUpperCase();\n}\n\nfunction MemberAvatar({ member }: { member: TeamMember }) {\n  const [broken, setBroken] = useState(false);\n\n  if (member.avatar && !broken) {\n    return (\n      // biome-ignore lint/performance/noImgElement: registry components stay framework-agnostic.\n      <img\n        alt={member.name}\n        className=\"size-12 shrink-0 rounded-full object-cover\"\n        height={48}\n        onError={() => setBroken(true)}\n        src={member.avatar}\n        width={48}\n      />\n    );\n  }\n\n  return (\n    <span className=\"flex size-12 shrink-0 items-center justify-center rounded-xl border border-foreground/8 bg-muted font-medium text-foreground text-sm\">\n      {initialsOf(member.name)}\n    </span>\n  );\n}\n\nfunction RoleSelect({\n  role,\n  roles,\n  onSelect,\n}: {\n  role: string;\n  roles: string[];\n  onSelect: (role: string) => void;\n}) {\n  const [open, setOpen] = useState(false);\n  const reduceMotion = useReducedMotion() === true;\n\n  return (\n    <div\n      className={cn(\"relative shrink-0\", open && \"z-[2147483647]\")}\n      onBlur={(event) => {\n        if (!event.currentTarget.contains(event.relatedTarget)) {\n          setOpen(false);\n        }\n      }}\n    >\n      <button\n        aria-expanded={open}\n        className=\"flex items-center gap-1.5 rounded-full border border-foreground/10 px-4 py-2 text-[13px] text-foreground transition-colors hover:bg-muted/60\"\n        onClick={() => setOpen((value) => !value)}\n        type=\"button\"\n      >\n        {role}\n        <motion.span\n          animate={{ rotate: open ? 180 : 0 }}\n          className=\"flex\"\n          transition={SPRING}\n        >\n          <ChevronDown className=\"size-3.5 text-muted-foreground\" />\n        </motion.span>\n      </button>\n      <AnimatePresence>\n        {open ? (\n          <motion.ul\n            animate={{ opacity: 1, scale: 1, y: 0 }}\n            className=\"absolute right-0 z-[2147483647] mt-2 min-w-36 origin-top-right rounded-xl border border-foreground/10 bg-card p-1 shadow-lg\"\n            exit={\n              reduceMotion ? { opacity: 0 } : { opacity: 0, scale: 0.94, y: -4 }\n            }\n            initial={\n              reduceMotion ? { opacity: 0 } : { opacity: 0, scale: 0.94, y: -4 }\n            }\n            transition={SPRING}\n          >\n            {roles.map((option) => (\n              <li key={option}>\n                <button\n                  className={cn(\n                    \"w-full rounded-lg px-3 py-1.5 text-left text-[13px] transition-colors hover:bg-muted\",\n                    option === role\n                      ? \"font-medium text-foreground\"\n                      : \"text-muted-foreground\"\n                  )}\n                  onClick={() => {\n                    onSelect(option);\n                    setOpen(false);\n                  }}\n                  type=\"button\"\n                >\n                  {option}\n                </button>\n              </li>\n            ))}\n          </motion.ul>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nfunction MemberRow({\n  member,\n  children,\n}: {\n  member: TeamMember;\n  children?: ReactNode;\n}) {\n  const reduceMotion = useReducedMotion() === true;\n  const panelValue = useContext(PanelValueContext);\n\n  return (\n    <motion.li\n      animate={{ opacity: 1, y: 0 }}\n      className=\"flex items-center gap-4\"\n      exit={reduceMotion ? { opacity: 0 } : { opacity: 0, scale: 0.96, x: -12 }}\n      initial={reduceMotion ? false : { opacity: 0, y: 14 }}\n      layout\n      layoutId={`team-member-${panelValue}-${member.id}`}\n      transition={MORPH_SPRING}\n    >\n      <MemberAvatar member={member} />\n      <span className=\"min-w-0 flex-1\">\n        <span className=\"block truncate font-medium text-[15px] text-foreground\">\n          {member.name}\n        </span>\n        <span className=\"block truncate text-[13px] text-muted-foreground\">\n          {member.email}\n        </span>\n      </span>\n      {children}\n    </motion.li>\n  );\n}\n\nfunction EmptyRow({ children }: { children: ReactNode }) {\n  const reduceMotion = useReducedMotion() === true;\n\n  return (\n    <motion.li\n      animate={{ opacity: 1 }}\n      className=\"rounded-xl border border-foreground/8 border-dashed px-4 py-3 text-[13px] text-muted-foreground\"\n      initial={reduceMotion ? false : { opacity: 0 }}\n      layout\n    >\n      {children}\n    </motion.li>\n  );\n}\n\nexport function TeamInvitationMemberList({\n  label,\n  action = \"role\",\n  emptyMessage,\n  className,\n}: {\n  label?: ReactNode;\n  /** Trailing control per row: a role dropdown, a role badge with remove, or nothing. */\n  action?: \"role\" | \"remove\" | \"none\";\n  emptyMessage?: ReactNode;\n  className?: string;\n}) {\n  const { members, roles, changeRole, removeMember } = useTeamInvitationContext(\n    \"TeamInvitationMemberList\"\n  );\n\n  return (\n    <div className={cn(\"mt-5\", className)}>\n      {label ? (\n        <TeamInvitationSectionLabel>{label}</TeamInvitationSectionLabel>\n      ) : null}\n      <ul className=\"mt-4 flex flex-col gap-4\">\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          {members.length > 0 ? (\n            members.map((member) => (\n              <MemberRow key={member.id} member={member}>\n                {action === \"role\" ? (\n                  <RoleSelect\n                    onSelect={(role) => changeRole(member.id, role)}\n                    role={member.role}\n                    roles={roles}\n                  />\n                ) : null}\n                {action === \"remove\" ? (\n                  <>\n                    <span className=\"rounded-full border border-foreground/10 px-3 py-1 text-[12px] text-muted-foreground\">\n                      {member.role}\n                    </span>\n                    <button\n                      aria-label={`Remove ${member.name}`}\n                      className=\"rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive\"\n                      onClick={() => removeMember(member.id)}\n                      type=\"button\"\n                    >\n                      <X className=\"size-4\" />\n                    </button>\n                  </>\n                ) : null}\n              </MemberRow>\n            ))\n          ) : emptyMessage ? (\n            <EmptyRow>{emptyMessage}</EmptyRow>\n          ) : null}\n        </AnimatePresence>\n      </ul>\n    </div>\n  );\n}\n\nexport function TeamInvitationRoleGroups({\n  emptyMessage,\n  className,\n}: {\n  /** Shown under a role heading when no member holds that role. */\n  emptyMessage?: ReactNode;\n  className?: string;\n}) {\n  const { members, roles, changeRole } = useTeamInvitationContext(\n    \"TeamInvitationRoleGroups\"\n  );\n\n  return (\n    <div className={className}>\n      {roles.map((role) => {\n        const group = members.filter((member) => member.role === role);\n\n        return (\n          <div className=\"mt-5\" key={role}>\n            <div className=\"flex items-center gap-2\">\n              <TeamInvitationSectionLabel>{role}</TeamInvitationSectionLabel>\n              <span className=\"rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground tabular-nums\">\n                {group.length}\n              </span>\n            </div>\n            <ul className=\"mt-3 flex flex-col gap-4\">\n              {group.length > 0 ? (\n                group.map((member) => (\n                  <MemberRow key={member.id} member={member}>\n                    <RoleSelect\n                      onSelect={(next) => changeRole(member.id, next)}\n                      role={member.role}\n                      roles={roles}\n                    />\n                  </MemberRow>\n                ))\n              ) : emptyMessage ? (\n                <EmptyRow>{emptyMessage}</EmptyRow>\n              ) : null}\n            </ul>\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n\nexport function TeamInvitationPendingList({\n  label,\n  emptyMessage,\n  className,\n}: {\n  label?: ReactNode;\n  emptyMessage?: ReactNode;\n  className?: string;\n}) {\n  const { pending, resend, cancelInvite } = useTeamInvitationContext(\n    \"TeamInvitationPendingList\"\n  );\n  const reduceMotion = useReducedMotion() === true;\n\n  return (\n    <div className={cn(\"mt-5\", className)}>\n      {label ? (\n        <TeamInvitationSectionLabel>{label}</TeamInvitationSectionLabel>\n      ) : null}\n      <ul className=\"mt-4 flex flex-col gap-4\">\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          {pending.length > 0 ? (\n            pending.map((invite) => (\n              <motion.li\n                animate={{ opacity: 1, y: 0 }}\n                className=\"flex items-center gap-4\"\n                exit={\n                  reduceMotion\n                    ? { opacity: 0 }\n                    : { opacity: 0, scale: 0.96, x: -12 }\n                }\n                initial={reduceMotion ? false : { opacity: 0, y: 14 }}\n                key={invite.id}\n                layout\n                transition={MORPH_SPRING}\n              >\n                <span className=\"flex size-12 shrink-0 items-center justify-center rounded-xl border border-foreground/8 text-muted-foreground\">\n                  <Clock className=\"size-5\" />\n                </span>\n                <span className=\"min-w-0 flex-1\">\n                  <span className=\"block truncate font-medium text-[14px] text-foreground\">\n                    {invite.email}\n                  </span>\n                  {invite.sentAt ? (\n                    <span className=\"block text-[13px] text-muted-foreground\">\n                      {invite.sentAt}\n                    </span>\n                  ) : null}\n                </span>\n                <button\n                  aria-label={`Resend invite to ${invite.email}`}\n                  className=\"rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground\"\n                  onClick={() => resend(invite.id)}\n                  type=\"button\"\n                >\n                  <RotateCw className=\"size-4\" />\n                </button>\n                <button\n                  aria-label={`Cancel invite to ${invite.email}`}\n                  className=\"rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive\"\n                  onClick={() => cancelInvite(invite.id)}\n                  type=\"button\"\n                >\n                  <X className=\"size-4\" />\n                </button>\n              </motion.li>\n            ))\n          ) : emptyMessage ? (\n            <EmptyRow>{emptyMessage}</EmptyRow>\n          ) : null}\n        </AnimatePresence>\n      </ul>\n    </div>\n  );\n}\n\nexport default TeamInvitation;\n",
      "type": "registry:ui"
    }
  ],
  "title": "Team Invitation",
  "description": "Team invitation block with fluid morphing variants for inviting members, managing the team, assigning roles, and tracking pending invites."
}
