{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "share-dialog",
  "type": "registry:ui",
  "title": "Share Dialog",
  "description": "Invite by email, manage who has access and set link access in one place.",
  "categories": [
    "collaboration"
  ],
  "dependencies": [
    "@base-ui/react",
    "@number-flow/react",
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/icons.json",
    "https://www.stealth.pm/r/motion.json",
    "https://www.stealth.pm/r/use-controllable-state.json",
    "https://www.stealth.pm/r/copy-button.json",
    "https://www.stealth.pm/r/invite-field.json"
  ],
  "files": [
    {
      "path": "components/ui/share-dialog.tsx",
      "content": "\"use client\";\nimport { Dialog } from \"@base-ui/react/dialog\";\nimport { Menu } from \"@base-ui/react/menu\";\nimport { Select } from \"@base-ui/react/select\";\nimport NumberFlow from \"@number-flow/react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useId, useState } from \"react\";\nimport { CopyButton } from \"@/components/ui/copy-button\";\nimport { InviteField, defaultInviteRoles, type InviteRole } from \"@/components/ui/invite-field\";\nimport { cn } from \"@/lib/cn\";\nimport { Check, ChevronDown, Globe, Lock, Mail, Trash, Users, X } from \"@/lib/icons\";\nimport { ease, spring } from \"@/lib/motion\";\nimport { useControllableState } from \"@/lib/use-controllable-state\";\n\n/* -------------------------------------------------------------------------------------------------\n * Types\n * -----------------------------------------------------------------------------------------------*/\n\nexport type SharePerson = {\n  id: string;\n  email: string;\n  /** Missing for people who haven't accepted an invite yet. */\n  name?: string;\n  avatarUrl?: string;\n  role: string;\n  /** Owners can't be changed or removed here. */\n  owner?: boolean;\n  /** Invited but not joined. */\n  pending?: boolean;\n};\n\nexport type ShareScope = \"restricted\" | \"organization\" | \"anyone\";\nexport type ShareAccess = { scope: ShareScope; role: string };\n\nconst initials = (name: string) =>\n  name\n    .trim()\n    .split(/\\s+/)\n    .slice(0, 2)\n    .map((w) => w[0]?.toUpperCase() ?? \"\")\n    .join(\"\");\n\nconst newId = () => `p-${Math.random().toString(36).slice(2, 9)}`;\n\n/* -------------------------------------------------------------------------------------------------\n * SharePanel: everything inside the dialog, usable on its own in a popover or a settings page\n * -----------------------------------------------------------------------------------------------*/\n\nexport type SharePanelProps = Omit<React.ComponentProps<\"div\">, \"children\" | \"defaultValue\" | \"onChange\"> & {\n  /** The link that general access applies to. */\n  link: string;\n  people?: SharePerson[];\n  defaultPeople?: SharePerson[];\n  onPeopleChange?: (people: SharePerson[]) => void;\n  access?: ShareAccess;\n  defaultAccess?: ShareAccess;\n  onAccessChange?: (access: ShareAccess) => void;\n  /** Roles for people and for the link. */\n  roles?: InviteRole[];\n  /** The id of the person looking, marked \"(you)\". */\n  currentUserId?: string;\n  /** Enables \"Anyone at {organizationName}\" and marks outside addresses in the invite field. */\n  organizationName?: string;\n  organizationDomain?: string;\n  /** Send invites. New people appear as pending once it resolves. */\n  onInvite?: (emails: string[], role: string) => void | Promise<unknown>;\n  /** Resend a pending invite. Shows a Resend action on pending rows. */\n  onResend?: (person: SharePerson) => void | Promise<unknown>;\n  /** Portal target for menus and lists, e.g. inside a contained demo. */\n  portalContainer?: HTMLElement | null;\n  /** Actions on the right of the footer, across from Copy link. */\n  footer?: React.ReactNode;\n};\n\nexport function SharePanel({\n  link,\n  people: peopleProp,\n  defaultPeople = [],\n  onPeopleChange,\n  access: accessProp,\n  defaultAccess = { scope: \"restricted\", role: \"viewer\" },\n  onAccessChange,\n  roles = defaultInviteRoles,\n  currentUserId,\n  organizationName,\n  organizationDomain,\n  onInvite,\n  onResend,\n  portalContainer,\n  footer,\n  className,\n  ...rest\n}: SharePanelProps) {\n  const reduce = !!useReducedMotion();\n  const uid = useId();\n  const [people, setPeople] = useControllableState({ value: peopleProp, defaultValue: defaultPeople, onChange: onPeopleChange });\n  const [access, setAccess] = useControllableState({ value: accessProp, defaultValue: defaultAccess, onChange: onAccessChange });\n  const [fresh, setFresh] = useState<string[]>([]);\n  const [announcement, setAnnouncement] = useState(\"\");\n\n  // New rows glow briefly, then settle, so you can see who you just added.\n  useEffect(() => {\n    if (!fresh.length) return;\n    const t = window.setTimeout(() => setFresh([]), 1600);\n    return () => window.clearTimeout(t);\n  }, [fresh]);\n\n  const invite = async (emails: string[], role: string) => {\n    await onInvite?.(emails, role);\n    const have = new Set(people.map((p) => p.email.toLowerCase()));\n    const added: SharePerson[] = emails.filter((e) => !have.has(e.toLowerCase())).map((email) => ({ id: newId(), email, role, pending: true }));\n    // People already here keep their row; their role is updated to the one just chosen.\n    const next = people.map((p) => (emails.some((e) => e.toLowerCase() === p.email.toLowerCase()) && !p.owner ? { ...p, role } : p));\n    // New people go in right under the owners, where the eye already is.\n    const owners = next.filter((p) => p.owner);\n    setPeople([...owners, ...added, ...next.filter((p) => !p.owner)]);\n    setFresh(added.map((p) => p.id));\n    setAnnouncement(`${emails.length === 1 ? emails[0] : `${emails.length} people`} can now ${roleLabel(role).toLowerCase().replace(/^can /, \"\")}`);\n  };\n\n  const roleLabel = (value: string) => roles.find((r) => r.value === value)?.label ?? value;\n\n  const changeRole = (id: string, role: string) => {\n    setPeople(people.map((p) => (p.id === id ? { ...p, role } : p)));\n    const who = people.find((p) => p.id === id);\n    if (who) setAnnouncement(`${who.name ?? who.email}: ${roleLabel(role)}`);\n  };\n\n  const removePerson = (id: string) => {\n    const who = people.find((p) => p.id === id);\n    setPeople(people.filter((p) => p.id !== id));\n    if (who) setAnnouncement(`Removed ${who.name ?? who.email}`);\n  };\n\n  const scopes: { value: ShareScope; label: string; note: (role: string) => string }[] = [\n    { value: \"restricted\", label: \"Restricted\", note: () => \"Only people with access can open the link\" },\n    ...(organizationName\n      ? [{ value: \"organization\" as const, label: `Anyone at ${organizationName}`, note: (r: string) => `Anyone at ${organizationName} with the link ${verb(r)}` }]\n      : []),\n    { value: \"anyone\", label: \"Anyone with the link\", note: (r: string) => `Anyone on the internet with the link ${verb(r)}` },\n  ];\n  const verb = (r: string) => (r === \"editor\" ? \"can edit\" : r === \"commenter\" ? \"can comment\" : \"can view\");\n  const scope = scopes.find((s) => s.value === access.scope) ?? scopes[0];\n\n  return (\n    <div className={cn(\"flex min-w-0 flex-col\", className)} {...rest}>\n      <InviteField label=\"\" roles={roles} organizationDomain={organizationDomain} onInvite={invite} portalContainer={portalContainer} placeholder=\"Add by email\" />\n\n      <section aria-labelledby={`${uid}-people`} className=\"mt-5 flex min-w-0 flex-col\">\n        <h3 id={`${uid}-people`} className=\"mb-1.5 flex items-center justify-between text-[12.5px] font-medium text-fg-2\">\n          People with access\n          <NumberFlow value={people.length} animated={!reduce} className=\"pr-2 font-normal text-fg-3 tabular\" />\n        </h3>\n        <ul className=\"-mx-2 flex max-h-[min(15rem,40vh)] flex-col overflow-y-auto overscroll-contain px-2 [scrollbar-width:thin] [mask-image:linear-gradient(to_bottom,transparent,black_6px,black_calc(100%-10px),transparent)]\">\n          <AnimatePresence initial={false}>\n            {people.map((p) => (\n              <motion.li\n                key={p.id}\n                initial={reduce ? { opacity: 0 } : { opacity: 0, height: 0 }}\n                animate={{ opacity: 1, height: \"auto\" }}\n                exit={reduce ? { opacity: 0 } : { opacity: 0, height: 0, transition: { height: { duration: 0.2, ease: ease.inOut }, opacity: { duration: 0.1 } } }}\n                transition={reduce ? { duration: 0.12 } : { height: { duration: 0.24, ease: ease.inOut }, opacity: { duration: 0.2, delay: 0.05 } }}\n                className=\"-mx-2 overflow-hidden px-2\"\n              >\n                <PersonRow\n                  person={p}\n                  you={p.id === currentUserId}\n                  fresh={fresh.includes(p.id)}\n                  roles={roles}\n                  roleLabel={roleLabel(p.role)}\n                  onRoleChange={(role) => changeRole(p.id, role)}\n                  onRemove={() => removePerson(p.id)}\n                  onResend={onResend ? () => onResend(p) : undefined}\n                  reduce={reduce}\n                  container={portalContainer}\n                />\n              </motion.li>\n            ))}\n          </AnimatePresence>\n        </ul>\n      </section>\n\n      <section aria-labelledby={`${uid}-general`} className=\"@container mt-4 flex min-w-0 flex-col\">\n        <h3 id={`${uid}-general`} className=\"mb-2 text-[12.5px] font-medium text-fg-2\">\n          General access\n        </h3>\n        {/* Wide: the role sits beside the scope. Narrow: it drops under the note, so neither truncates. */}\n        <div className=\"grid min-w-0 grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-x-3 [grid-template-areas:'icon_scope_role''icon_note_note'] @max-[26rem]:[grid-template-areas:'icon_scope_scope''icon_note_note''icon_role_role']\">\n          <span\n            aria-hidden\n            className={cn(\n              \"relative grid size-9 shrink-0 self-center [grid-area:icon] @max-[26rem]:self-start place-items-center overflow-hidden rounded-full transition-colors duration-300\",\n              access.scope === \"restricted\" ? \"bg-fg/[0.06] text-fg-2\" : \"bg-success-soft text-success\",\n            )}\n          >\n            <AnimatePresence initial={false} mode=\"popLayout\">\n              <motion.span\n                key={access.scope}\n                initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.5, rotate: -20 }}\n                animate={{ opacity: 1, scale: 1, rotate: 0 }}\n                exit={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.5, rotate: 20 }}\n                transition={reduce ? { duration: 0.12 } : spring.pop}\n                className=\"grid place-items-center\"\n              >\n                {access.scope === \"restricted\" ? <Lock /> : access.scope === \"organization\" ? <Users /> : <Globe />}\n              </motion.span>\n            </AnimatePresence>\n          </span>\n\n          <div className=\"-ml-1.5 flex min-w-0 items-center gap-1 [grid-area:scope]\">\n            <InlineSelect\n              label=\"Who can open the link\"\n              value={access.scope}\n              items={scopes.map((s) => ({ value: s.value, label: s.label }))}\n              onValueChange={(v) => {\n                setAccess({ ...access, scope: v as ShareScope });\n                setAnnouncement(scopes.find((s) => s.value === v)?.label ?? \"\");\n              }}\n              strong\n              container={portalContainer}\n            />\n          </div>\n          <div className=\"min-w-0 [grid-area:note]\">\n            <AnimatePresence initial={false} mode=\"popLayout\">\n              <motion.p\n                key={`${access.scope}-${access.role}`}\n                initial={reduce ? { opacity: 0 } : { opacity: 0, y: 3 }}\n                animate={{ opacity: 1, y: 0 }}\n                exit={{ opacity: 0, transition: { duration: 0.08 } }}\n                transition={{ duration: 0.18, ease: ease.out }}\n                className=\"text-[12px] leading-4 text-pretty text-fg-3\"\n              >\n                {scope.note(access.role)}\n              </motion.p>\n            </AnimatePresence>\n          </div>\n\n          <AnimatePresence initial={false} mode=\"popLayout\">\n            {access.scope !== \"restricted\" && (\n              <motion.div\n                key=\"link-role\"\n                initial={reduce ? { opacity: 0 } : { opacity: 0, x: 8, filter: \"blur(2px)\" }}\n                animate={{ opacity: 1, x: 0, filter: \"blur(0px)\" }}\n                exit={reduce ? { opacity: 0 } : { opacity: 0, x: 8, filter: \"blur(2px)\", transition: { duration: 0.12 } }}\n                transition={{ duration: 0.22, ease: ease.out }}\n                className=\"justify-self-end [grid-area:role] @max-[26rem]:-ml-1.5 @max-[26rem]:mt-1 @max-[26rem]:justify-self-start\"\n              >\n                <InlineSelect\n                  label=\"What people with the link can do\"\n                  value={access.role}\n                  items={roles.map((r) => ({ value: r.value, label: r.label, description: r.description }))}\n                  onValueChange={(v) => setAccess({ ...access, role: v })}\n                  align=\"end\"\n                  container={portalContainer}\n                />\n              </motion.div>\n            )}\n          </AnimatePresence>\n        </div>\n      </section>\n\n      <div className=\"mt-5 flex items-center justify-between gap-2\">\n        <CopyButton value={link} label=\"Copy link\" copiedLabel=\"Link copied\" failedLabel=\"Couldn’t copy\" />\n        {footer}\n      </div>\n\n      <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {announcement}\n      </span>\n    </div>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * One person with access\n * -----------------------------------------------------------------------------------------------*/\n\nfunction PersonRow({\n  person: p,\n  you,\n  fresh,\n  roles,\n  roleLabel,\n  onRoleChange,\n  onRemove,\n  onResend,\n  reduce,\n  container,\n}: {\n  person: SharePerson;\n  you: boolean;\n  fresh: boolean;\n  roles: InviteRole[];\n  roleLabel: string;\n  onRoleChange: (role: string) => void;\n  onRemove: () => void;\n  onResend?: () => void | Promise<unknown>;\n  reduce: boolean;\n  container?: HTMLElement | null;\n}) {\n  const [resent, setResent] = useState<\"idle\" | \"sending\" | \"done\">(\"idle\");\n  const display = p.name ?? p.email;\n\n  const resend = async () => {\n    if (!onResend || resent !== \"idle\") return;\n    setResent(\"sending\");\n    try {\n      await onResend();\n      setResent(\"done\");\n    } catch {\n      setResent(\"idle\");\n    }\n  };\n\n  return (\n    <div\n      data-fresh={fresh || undefined}\n      className={cn(\n        \"relative flex min-h-12 min-w-0 items-center gap-3 rounded-lg py-1.5\",\n        // A just-added row starts lit and cools to the page over a second.\n        \"before:pointer-events-none before:absolute before:-inset-x-2 before:inset-y-0.5 before:rounded-lg before:bg-fg/[0.06] before:opacity-0 before:transition-opacity before:duration-1000\",\n        \"data-fresh:before:opacity-100 data-fresh:before:duration-0\",\n      )}\n    >\n      <span\n        aria-hidden\n        className={cn(\n          \"relative grid size-8 shrink-0 select-none place-items-center overflow-hidden rounded-full text-[11.5px] font-medium\",\n          p.pending ? \"border border-dashed border-line-2 text-fg-3\" : \"bg-hover text-fg-2 shadow-[inset_0_0_0_1px_var(--line-2)]\",\n        )}\n      >\n        {p.avatarUrl ? (\n          // eslint-disable-next-line @next/next/no-img-element\n          <img src={p.avatarUrl} alt=\"\" className=\"size-full object-cover\" />\n        ) : p.pending || !p.name ? (\n          <Mail size={14} />\n        ) : (\n          initials(p.name)\n        )}\n      </span>\n\n      <div className=\"flex min-w-0 flex-1 flex-col\">\n        <p className=\"flex min-w-0 items-baseline gap-1 text-[13px] leading-5 text-fg\">\n          <span className={cn(\"min-w-0 font-medium\", p.name ? \"truncate\" : \"[overflow-wrap:anywhere]\")}>{display}</span>\n          {you && <span className=\"shrink-0 text-fg-3\">(you)</span>}\n        </p>\n        <p className=\"flex min-w-0 items-center gap-1.5 text-[12px] leading-4 text-fg-3\">\n          {p.pending ? (\n            <>\n              <span className=\"shrink-0\">Invite sent</span>\n              {onResend && (\n                <>\n                  <span aria-hidden className=\"text-fg-4\">\n                    ·\n                  </span>\n                  <button\n                    type=\"button\"\n                    onClick={() => void resend()}\n                    disabled={resent !== \"idle\"}\n                    className={cn(\n                      \"relative rounded-sm text-fg-2 underline decoration-fg-4 underline-offset-2 outline-none transition-colors duration-150 hover:text-fg hover:decoration-fg-3\",\n                      \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-fg-3\",\n                      \"disabled:no-underline disabled:text-fg-3\",\n                      \"before:absolute before:-inset-x-1 before:-inset-y-3 before:content-[''] pointer-fine:before:hidden\",\n                    )}\n                  >\n                    {resent === \"done\" ? \"Sent again\" : resent === \"sending\" ? \"Sending…\" : \"Resend\"}\n                  </button>\n                </>\n              )}\n            </>\n          ) : (\n            p.name && <span className=\"min-w-0 truncate\">{p.email}</span>\n          )}\n        </p>\n      </div>\n\n      {p.owner ? (\n        <span className=\"shrink-0 pr-2 text-[12.5px] text-fg-3\">Owner</span>\n      ) : (\n        <Menu.Root>\n          <Menu.Trigger\n            aria-label={`Access for ${display}: ${roleLabel}`}\n            className={cn(\n              \"group/role relative flex h-7 shrink-0 items-center gap-1 rounded-md pl-2 pr-1.5 text-[12.5px] text-fg-2 outline-none select-none\",\n              \"transition-[background-color,color,scale] duration-150 ease-out-quart hover:bg-fg/[0.06] hover:text-fg active:scale-[0.97] active:duration-75\",\n              \"data-popup-open:bg-fg/[0.08] data-popup-open:text-fg\",\n              \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-0 focus-visible:outline-fg-3\",\n              \"before:absolute before:-inset-y-2 before:inset-x-0 before:content-[''] pointer-fine:before:hidden\",\n            )}\n          >\n            <span className=\"grid overflow-hidden\">\n              <AnimatePresence initial={false} mode=\"popLayout\">\n                <motion.span\n                  key={roleLabel}\n                  initial={reduce ? { opacity: 0 } : { opacity: 0, y: 8, filter: \"blur(2px)\" }}\n                  animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n                  exit={reduce ? { opacity: 0 } : { opacity: 0, y: -8, filter: \"blur(2px)\" }}\n                  transition={{ duration: 0.2, ease: ease.out }}\n                  className=\"col-start-1 row-start-1 whitespace-nowrap\"\n                >\n                  {roleLabel}\n                </motion.span>\n              </AnimatePresence>\n            </span>\n            <ChevronDown size={14} className=\"text-fg-3 transition-transform duration-200 ease-out-expo group-data-popup-open/role:rotate-180 motion-reduce:transition-none\" />\n          </Menu.Trigger>\n          <Menu.Portal container={container}>\n            <Menu.Positioner side=\"bottom\" align=\"end\" sideOffset={4} collisionPadding={8} className=\"z-(--z-popover)\">\n              <Menu.Popup className={popup}>\n                <Menu.RadioGroup value={p.role} onValueChange={(v) => onRoleChange(String(v))}>\n                  {roles.map((r) => (\n                    <Menu.RadioItem key={r.value} value={r.value} closeOnClick className={item}>\n                      <span className=\"flex min-w-0 flex-1 flex-col\">\n                        <span className=\"truncate text-fg\">{r.label}</span>\n                        {r.description && <span className=\"truncate text-[12px] leading-4 text-fg-3\">{r.description}</span>}\n                      </span>\n                      <Menu.RadioItemIndicator className=\"grid size-4 shrink-0 place-items-center\">\n                        <Check size={14} />\n                      </Menu.RadioItemIndicator>\n                    </Menu.RadioItem>\n                  ))}\n                </Menu.RadioGroup>\n                <Menu.Separator className=\"mx-2 my-1 h-px bg-line\" />\n                <Menu.Item onClick={onRemove} className={cn(item, \"min-h-8 text-danger data-highlighted:bg-danger-soft\")}>\n                  <Trash size={16} className=\"shrink-0\" />\n                  <span className=\"flex-1\">{p.pending ? \"Cancel invite\" : \"Remove access\"}</span>\n                </Menu.Item>\n              </Menu.Popup>\n            </Menu.Positioner>\n          </Menu.Portal>\n        </Menu.Root>\n      )}\n    </div>\n  );\n}\n\nconst popup = cn(\n  \"w-60 max-w-(--available-width) origin-(--transform-origin) rounded-xl border border-line-2 bg-raised p-1 text-fg shadow-pop outline-none\",\n  \"transition-[opacity,scale,translate] duration-160 ease-out-expo\",\n  \"data-starting-style:-translate-y-1 data-starting-style:scale-96 data-starting-style:opacity-0\",\n  \"data-ending-style:scale-98 data-ending-style:opacity-0 data-ending-style:duration-100\",\n  \"data-instant:transition-none motion-reduce:data-starting-style:translate-y-0 motion-reduce:data-starting-style:scale-100\",\n);\n\nconst item = cn(\n  \"flex min-h-11 cursor-default select-none items-center gap-2.5 rounded-lg px-2 py-1.5 text-[13px] outline-none\",\n  \"transition-colors duration-75 data-highlighted:bg-fg/[0.06]\",\n);\n\n/** A borderless select that reads as text until hovered: used for the link scope and role. */\nfunction InlineSelect({\n  label,\n  value,\n  items,\n  onValueChange,\n  strong,\n  align = \"start\",\n  container,\n}: {\n  label: string;\n  value: string;\n  items: { value: string; label: string; description?: string }[];\n  onValueChange: (v: string) => void;\n  strong?: boolean;\n  align?: \"start\" | \"end\";\n  container?: HTMLElement | null;\n}) {\n  return (\n    <Select.Root items={items} value={value} onValueChange={(v) => v != null && onValueChange(String(v))}>\n      <Select.Trigger\n        aria-label={label}\n        className={cn(\n          \"group/sel relative flex h-7 min-w-0 items-center gap-1 rounded-md px-1.5 text-[13px] outline-none select-none\",\n          \"transition-[background-color,color,scale] duration-150 ease-out-quart hover:bg-fg/[0.06] active:scale-[0.98] active:duration-75\",\n          \"data-popup-open:bg-fg/[0.08]\",\n          \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-0 focus-visible:outline-fg-3\",\n          \"before:absolute before:-inset-y-2 before:inset-x-0 before:content-[''] pointer-fine:before:hidden\",\n          strong ? \"font-medium text-fg\" : \"text-fg-2 hover:text-fg\",\n        )}\n      >\n        <Select.Value className=\"min-w-0 truncate\" />\n        <Select.Icon className=\"flex shrink-0 text-fg-3 transition-transform duration-200 ease-out-expo group-data-popup-open/sel:rotate-180 motion-reduce:transition-none\">\n          <ChevronDown size={14} />\n        </Select.Icon>\n      </Select.Trigger>\n      <Select.Portal container={container}>\n        <Select.Positioner side=\"bottom\" align={align} sideOffset={6} collisionPadding={8} alignItemWithTrigger={false} className=\"z-(--z-popover) outline-none\">\n          <Select.Popup className={popup}>\n            <Select.List>\n              {items.map((i) => (\n                <Select.Item key={i.value} value={i.value} className={cn(item, !i.description && \"min-h-9\")}>\n                  <span className=\"flex min-w-0 flex-1 flex-col\">\n                    <Select.ItemText className=\"truncate text-fg\">{i.label}</Select.ItemText>\n                    {i.description && <span className=\"truncate text-[12px] leading-4 text-fg-3\">{i.description}</span>}\n                  </span>\n                  <Select.ItemIndicator className=\"grid size-4 shrink-0 place-items-center\">\n                    <Check size={14} />\n                  </Select.ItemIndicator>\n                </Select.Item>\n              ))}\n            </Select.List>\n          </Select.Popup>\n        </Select.Positioner>\n      </Select.Portal>\n    </Select.Root>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * ShareDialog\n * -----------------------------------------------------------------------------------------------*/\n\nexport type ShareDialogProps = Omit<Dialog.Root.Props, \"children\"> &\n  Omit<SharePanelProps, \"className\" | \"portalContainer\"> & {\n    /** The button that opens it. Rendered as the dialog trigger. */\n    trigger?: React.ReactElement<Record<string, unknown>>;\n    /** Name the thing: “Share q3-forecast.xlsx”. */\n    title: React.ReactNode;\n    description?: React.ReactNode;\n    /** Render inside this element instead of document.body; the backdrop then covers only it. */\n    container?: HTMLElement | null;\n    /** Where focus goes on open. Defaults to the invite field. */\n    initialFocus?: Dialog.Popup.Props[\"initialFocus\"];\n    className?: string;\n  };\n\nexport function ShareDialog({\n  open,\n  defaultOpen,\n  onOpenChange,\n  modal,\n  disablePointerDismissal,\n  trigger,\n  title,\n  description,\n  container,\n  initialFocus,\n  className,\n  ...panel\n}: ShareDialogProps) {\n  const contained = container != null;\n  return (\n    <Dialog.Root open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange} modal={modal} disablePointerDismissal={disablePointerDismissal}>\n      {trigger && <Dialog.Trigger render={trigger} />}\n      <Dialog.Portal container={container}>\n        <Dialog.Backdrop\n          className={cn(\n            contained ? \"absolute\" : \"fixed\",\n            \"inset-0 z-(--z-overlay) bg-overlay transition-opacity duration-200 ease-out-quart data-starting-style:opacity-0 data-ending-style:opacity-0 data-ending-style:duration-150\",\n          )}\n        />\n        <Dialog.Viewport\n          className={cn(\n            contained ? \"absolute\" : \"fixed\",\n            \"inset-0 z-(--z-dialog) flex items-center justify-center p-4 max-sm:items-end max-sm:p-2 max-sm:pb-[max(8px,env(safe-area-inset-bottom))]\",\n          )}\n        >\n          <Dialog.Popup\n            initialFocus={initialFocus}\n            className={cn(\n              \"relative flex max-h-full w-full max-w-[520px] min-w-0 flex-col overflow-y-auto overscroll-contain rounded-2xl border border-line-2 bg-raised p-5 text-fg shadow-pop outline-none\",\n              \"origin-center transition-[opacity,scale,translate] duration-[260ms] ease-out-expo\",\n              \"data-starting-style:translate-y-1.5 data-starting-style:scale-[0.97] data-starting-style:opacity-0\",\n              \"data-ending-style:scale-[0.98] data-ending-style:opacity-0 data-ending-style:duration-[160ms] data-ending-style:ease-out-quart\",\n              \"max-sm:data-starting-style:translate-y-4 max-sm:data-starting-style:scale-100 motion-reduce:data-starting-style:translate-y-0 motion-reduce:data-starting-style:scale-100\",\n              className,\n            )}\n          >\n            <div className=\"mb-4 flex min-w-0 flex-col gap-1 pr-8\">\n              <Dialog.Title className=\"min-w-0 text-[15px] font-medium leading-[1.3] tracking-[-0.015em] text-balance text-fg\">{title}</Dialog.Title>\n              {description && <Dialog.Description className=\"text-[12.5px] leading-[1.5] text-fg-3\">{description}</Dialog.Description>}\n            </div>\n\n            <SharePanel\n              {...panel}\n              portalContainer={container}\n              footer={\n                <Dialog.Close\n                  className={cn(\n                    \"inline-flex h-8 items-center justify-center rounded-lg bg-fg px-3 text-[12.5px] font-medium text-frame shadow-[var(--shadow)] outline-none\",\n                    \"transition-[background-color,scale] duration-150 ease-out-quart hover:bg-fg/90 active:scale-[0.97] active:duration-75\",\n                    \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n                  )}\n                >\n                  Done\n                </Dialog.Close>\n              }\n            />\n            <Dialog.Close\n              aria-label=\"Close\"\n              className={cn(\n                \"absolute right-3 top-3 grid size-7 place-items-center rounded-md text-fg-3 outline-none\",\n                \"transition-[background-color,color,scale] duration-150 ease-out-quart hover:bg-fg/[0.06] hover:text-fg active:scale-[0.92] active:duration-75\",\n                \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n                \"before:absolute before:-inset-2 before:content-[''] pointer-fine:before:hidden\",\n              )}\n            >\n              <X />\n            </Dialog.Close>\n          </Dialog.Popup>\n        </Dialog.Viewport>\n      </Dialog.Portal>\n    </Dialog.Root>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/share-dialog"
}
