Transfer Ownership

Reads back what an ownership transfer moves and keeps, warns about an outside email or a recipient whose plan can't own it, and waits for acceptance.

StopsOwnership transferred to the wrong email

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/transfer-ownership.json

The files land in your project. They're yours to edit.

Usage

transfer-ownership-usage.tsx
import { TransferOwnership, type TransferRequest } from "@/components/ui/transfer-ownership"

<TransferOwnership
  resource="Acme workspace"
  orgDomain="acme.in"
  members={MEMBERS}
  requiredPlan="Team"
  defaultValue="priya.nair@gmail.com"
  moves={["Billing and the ₹24,000/yr plan", "Deleting the workspace", "Owner role"]}
  keeps={["Admin access", "Your personal drafts"]}
  request={request}
  onRequestChange={setRequest}
  onSend={() => new Promise<void>((resolve) => setTimeout(resolve, 500))}
  onWithdraw={() => new Promise<void>((resolve) => setTimeout(resolve, 300))}
/>

What it catches

  • Recipient email outside your organisation, with a likely internal match to switch to
  • Recipient plan too low to own the resource
  • What moves to them and what stays with you
  • Pending request and its expiry, with withdraw

Props

TransferOwnership props
PropTypeDefault
orgDomainstring—
membersOrgMember[]—
requiredPlanstring—
plans

Plans from lowest to highest, used to compare against `requiredPlan`.

string[]—
resource* (required)

What is being handed over, e.g. "Acme workspace".

string—
moves* (required)

What moves to the new owner.

string[]—
keeps* (required)

What stays with you, e.g. "Admin access", "Your personal drafts".

string[]—
request

Controlled request. `null` means no transfer has been sent.

TransferRequest | null—
defaultRequest

Starting pending request when uncontrolled.

TransferRequest | nullnull
onRequestChange

Called when a request is sent or withdrawn.

(request: TransferRequest | null) => void—
onSend* (required)

Sends the request. Return a promise to show a pending state; a rejection keeps the form.

(recipient: { email: string; name?: string }) => void | Promise<void>—
value

Controlled recipient email.

string—
defaultValue

Starting recipient email when uncontrolled.

string""
onValueChange

Called with the recipient email.

(email: string) => void—
onWithdraw

Withdraws a pending request.

() => void | Promise<void>—
expiresInDays

How long the recipient has to accept.

number7
title

Heading, given the resource name.

(resource: string) => React.ReactNode(r) => `Transfer ownership of ${r}`
label

Label of the recipient email input.

string"New owner's email"
sendLabel

Label of the send-request button.

string"Send transfer request"
externalText

Warning for an email outside the organisation.

(domain: string) => string(d) => `${d} is outside your organization. They'd own thi…
externalAck

Checkbox text to confirm an outside recipient.

(domain: string) => string(d) => `I mean to hand this to someone at ${d}`
planText

Warning when the recipient's plan is too low.

(name: string, plan: string, required: string) => string(n, p, r) => `${n} is on ${p}. Owning this needs ${r}, so…
statusText

Status line once a request exists. `who` is their name or email.

(status: TransferStatus, who: string, resource: string, daysLeft: number) => string(status, who, r, days) => status === "accepted" ? `${who}…
noteText

Line under the readback before sending.

(expiresInDays: number) => string(days) => `Nothing moves until they accept. They have ${p…
withdrawLabel

Label of the withdraw button.

string"Withdraw request"
chooseAgainLabel

Label of the choose-someone-else button.

string"Choose someone else"

* required. Also accepts the native props of <section> (className, aria-*, data-*…), passed through to it.

Accessibility

  • Section labelled by its title; email input has a <label>, aria-invalid on plan issues and a polite hint
  • Blocking issues use role=alert
  • Withdraw sets aria-busy while pending; state changes in a polite live region

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

This is a UX guard rail. Enforce on the server: Authorize the transfer server-side, require the recipient's acceptance, enforce plan eligibility and expire stale requests.

  • Recipient and plan data come from your props
  • The accept-before-transfer flow must be implemented by your backend

What it stops

Ownership transferred to the wrong email

Also known as

transfer ownership · change owner · workspace owner · admin · transfer repo · invite · handover

Field type

Dependencies

Pairs well with