{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "share-permissions-popover",
  "type": "registry:component",
  "title": "Share & Permissions Popover",
  "description": "A three-view floating surface that morphs on both axes with a focus trap surviving the swaps.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://lab.moumen.dev/r/lab-theme.json"
  ],
  "files": [
    {
      "path": "registry/lab/share-permissions-popover/share-permissions-popover.tsx",
      "type": "registry:component",
      "target": "@components/lab/share-permissions-popover.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useLayoutEffect, useRef, useState, type CSSProperties } from \"react\";\nimport { MotionConfig, motion, useReducedMotion } from \"motion/react\";\n\n// Share & permissions popover - three views in one floating element.\n//\n// A popover is a FLOATING element with three different-sized panels inside\n// (share list · a person's role picker · link settings), and swapping panels\n// has to feel like one surface changing shape, not three popovers taking turns.\n//\n//   · Animated auto-size on BOTH axes. Each view declares its natural width and\n//     grows its own height; views are absolutely positioned so they keep their\n//     intrinsic size, and the frame's width AND height are measured px\n//     (useLayoutEffect + ResizeObserver) that motion eases between - the\n//     height:auto illusion, on two axes. A fresh open snaps to size instead of\n//     morphing from the last session's.\n//   · A focus trap that survives view swaps. Tab cycles the LIVE view's\n//     controls only; every swap hands focus somewhere sensible (push → the\n//     current role; pop → the row that opened it; close → the trigger). Pass\n//     trap={false} to feel Tab walk out.\n//   · Breadcrumb back-navigation. Escape backs out one layer at a time.\n//\n// The popover scales in FROM THE TRIGGER (origin under the Share button), stays\n// mounted so open/close are interruptible, and slides views directionally:\n// push enters from the right, pop from the left, with a short-lived leaving\n// snapshot. Animation via motion/react; honours prefers-reduced-motion.\n// Requires the lab-theme tokens. Fully Tailwind, no CSS files.\n\nconst EASE = [0.22, 1, 0.36, 1] as const;\nconst EASE_ICON = [0.2, 0, 0, 1] as const;\nconst SLIDE = 28; // 1.75rem directional slide distance\n\nexport interface Person {\n  id: string;\n  name: string;\n  email: string;\n}\n\nexport interface SharePopoverState {\n  open: boolean;\n  view: string;\n  people: number;\n  link: string;\n  size: { w: number; h: number } | null;\n  lastAction: string | null;\n}\n\ntype View = { name: \"share\" } | { name: \"role\"; person: Person } | { name: \"link\" };\n\nconst ROLES = [\n  { id: \"full\", label: \"Full access\", hint: \"Edit, share and manage\" },\n  { id: \"edit\", label: \"Can edit\", hint: \"Edit but not share\" },\n  { id: \"comment\", label: \"Can comment\", hint: \"Read and comment\" },\n  { id: \"view\", label: \"Can view\", hint: \"Read only\" },\n];\n\nconst LINK_SCOPES = [\n  { id: \"anyone\", label: \"Anyone with the link\", hint: \"No sign-in needed\", icon: \"globe\" as const },\n  { id: \"invited\", label: \"Invited people only\", hint: \"Must be on this list\", icon: \"users\" as const },\n  { id: \"off\", label: \"No link access\", hint: \"Only direct invites\", icon: \"lock\" as const },\n];\n\nfunction initials(name: string) {\n  return name.split(\" \").map((w) => w[0]).slice(0, 2).join(\"\").toUpperCase();\n}\n\n// \"sarah.chen@acme.co\" → \"Sarah Chen\" · \"Priya\" → priya@acme.co\nfunction parseInvite(raw: string): { name: string; email: string } | null {\n  const text = raw.trim();\n  if (!text) return null;\n  if (text.includes(\"@\")) {\n    const name = text.split(\"@\")[0].split(/[._-]+/).filter(Boolean).map((p) => p[0].toUpperCase() + p.slice(1)).join(\" \");\n    return { name: name || text, email: text };\n  }\n  return { name: text, email: `${text.toLowerCase().replace(/\\s+/g, \".\")}@acme.co` };\n}\n\nconst roleLabel = (id: string) => ROLES.find((r) => r.id === id)?.label ?? id;\nconst scopeLabel = (id: string) => LINK_SCOPES.find((s) => s.id === id)?.label ?? id;\n\nconst OPT =\n  \"group/opt flex items-center gap-2 w-full p-1.5 rounded-lg bg-transparent text-left cursor-pointer transition-colors duration-150 hover:bg-accent focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring\";\n\nexport default function SharePopover({\n  people: initialPeople = INITIAL_PEOPLE,\n  initialRoles = INITIAL_ROLES,\n  owner = { name: \"Moumen Soliman\", email: \"moumen@acme.co\" },\n  docTitle = \"Q3 Launch Plan\",\n  onShare,\n  morph = true,\n  trap = true,\n  inspect = false,\n  onStateChange,\n}: {\n  people?: Person[];\n  initialRoles?: Record<string, string>;\n  owner?: { name: string; email: string };\n  docTitle?: string;\n  /** Fires on any change (invite, role, remove, link scope) - wire your backend here. */\n  onShare?: (event: { type: \"invite\" | \"role\" | \"remove\" | \"link\"; detail: string }) => void;\n  morph?: boolean;\n  trap?: boolean;\n  inspect?: boolean;\n  onStateChange?: (state: SharePopoverState) => void;\n}) {\n  const [open, setOpen] = useState(false);\n  const [view, setView] = useState<View>({ name: \"share\" });\n  const [people, setPeople] = useState<Person[]>(initialPeople);\n  const [roles, setRoles] = useState<Record<string, string>>(initialRoles);\n  const [linkScope, setLinkScope] = useState(\"invited\");\n  const [invite, setInvite] = useState(\"\");\n  const [copied, setCopied] = useState(false);\n  const [leaving, setLeaving] = useState<{ view: View; dir: number } | null>(null);\n  const [dims, setDims] = useState<{ w: number; h: number } | null>(null);\n  const [lastAction, setLastAction] = useState<string | null>(null);\n\n  const rootRef = useRef<HTMLDivElement>(null);\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const frameRef = useRef<HTMLDivElement>(null);\n  const viewRef = useRef<HTMLDivElement>(null);\n  const leaveTimerRef = useRef<number>(0);\n  const copyTimerRef = useRef<number>(0);\n  const freshRef = useRef(false);\n  const snapRef = useRef(false);\n  const pendingFocusRef = useRef<string | null>(null);\n  const reduced = useReducedMotion();\n\n  const viewKey = view.name === \"role\" ? `role:${view.person.id}` : view.name;\n\n  function openPop() {\n    freshRef.current = true;\n    pendingFocusRef.current = \"[data-invite-input]\";\n    setView({ name: \"share\" });\n    setLeaving(null);\n    setOpen(true);\n  }\n\n  function closePop(returnFocus: boolean) {\n    setOpen(false);\n    setLeaving(null);\n    if (returnFocus) triggerRef.current?.focus({ preventScroll: true });\n  }\n\n  function navigate(nextView: View, dir: number, focusSelector?: string) {\n    if (morph && !reduced) {\n      setLeaving({ view, dir });\n      clearTimeout(leaveTimerRef.current);\n      leaveTimerRef.current = window.setTimeout(() => setLeaving(null), 320);\n    }\n    pendingFocusRef.current = focusSelector ?? null;\n    setView(nextView);\n  }\n\n  const goRole = (person: Person) => navigate({ name: \"role\", person }, 1, `[data-role-opt=\"${roles[person.id]}\"]`);\n  const goLink = () => navigate({ name: \"link\" }, 1, `[data-link-opt=\"${linkScope}\"]`);\n  const backFromRole = (person: Person) => navigate({ name: \"share\" }, -1, `[data-person=\"${person.id}\"]`);\n  const backFromLink = () => navigate({ name: \"share\" }, -1, \"[data-link-row]\");\n\n  function handleInvite() {\n    const parsed = parseInvite(invite);\n    if (!parsed) return;\n    const id = `p${Date.now()}`;\n    setPeople((list) => [...list, { id, ...parsed }]);\n    setRoles((map) => ({ ...map, [id]: \"edit\" }));\n    setInvite(\"\");\n    setLastAction(`Invited ${parsed.name} · Can edit`);\n    onShare?.({ type: \"invite\", detail: `${parsed.name} <${parsed.email}>` });\n  }\n\n  function pickRole(person: Person, roleId: string) {\n    setRoles((map) => ({ ...map, [person.id]: roleId }));\n    setLastAction(`${person.name} → ${roleLabel(roleId)}`);\n    onShare?.({ type: \"role\", detail: `${person.name}: ${roleLabel(roleId)}` });\n    backFromRole(person);\n  }\n\n  function removePerson(person: Person) {\n    setPeople((list) => list.filter((p) => p.id !== person.id));\n    setLastAction(`Removed ${person.name}`);\n    onShare?.({ type: \"remove\", detail: person.name });\n    navigate({ name: \"share\" }, -1, \"[data-invite-input]\");\n  }\n\n  function pickScope(scopeId: string) {\n    setLinkScope(scopeId);\n    setLastAction(`Link: ${scopeLabel(scopeId)}`);\n    onShare?.({ type: \"link\", detail: scopeLabel(scopeId) });\n    backFromLink();\n  }\n\n  function copyLink() {\n    try {\n      navigator.clipboard?.writeText(\"https://acme.co/doc/q3-launch-plan\");\n    } catch {\n      /* the demo doesn't care */\n    }\n    setCopied(true);\n    setLastAction(\"Link copied\");\n    clearTimeout(copyTimerRef.current);\n    copyTimerRef.current = window.setTimeout(() => setCopied(false), 1400);\n  }\n\n  // The two-axis height:auto illusion - the live view keeps its intrinsic size;\n  // the frame is told the measured px for BOTH axes so motion eases between\n  // them. A fresh open snaps (duration 0) instead of morphing from the last one.\n  useLayoutEffect(() => {\n    const node = viewRef.current;\n    if (!open || !node) return undefined;\n    const apply = () => {\n      const w = node.offsetWidth;\n      const h = node.offsetHeight;\n      setDims({ w, h });\n    };\n    if (freshRef.current) {\n      freshRef.current = false;\n      snapRef.current = true;\n      apply();\n      requestAnimationFrame(() => {\n        snapRef.current = false;\n      });\n    } else {\n      apply();\n    }\n    const observer = typeof ResizeObserver !== \"undefined\" ? new ResizeObserver(apply) : null;\n    observer?.observe(node);\n    return () => observer?.disconnect();\n  }, [open, viewKey, people.length]);\n\n  // Focus choreography: after every open/swap, hand focus to the promised control.\n  useLayoutEffect(() => {\n    if (!open) return;\n    const selector = pendingFocusRef.current;\n    pendingFocusRef.current = null;\n    if (!selector) return;\n    viewRef.current?.querySelector<HTMLElement>(selector)?.focus({ preventScroll: true });\n  }, [open, viewKey]);\n\n  // The trap: Tab cycles the LIVE view's controls only (the leaving snapshot is inert).\n  function handleTrapKeys(event: React.KeyboardEvent) {\n    if (event.key === \"Escape\") {\n      event.stopPropagation();\n      if (view.name === \"role\") backFromRole(view.person);\n      else if (view.name === \"link\") backFromLink();\n      else closePop(true);\n      return;\n    }\n    if (!trap || event.key !== \"Tab\") return;\n    const focusables = viewRef.current\n      ? [...viewRef.current.querySelectorAll<HTMLElement>(\"button:not(:disabled), input:not(:disabled)\")]\n      : [];\n    if (focusables.length === 0) {\n      event.preventDefault();\n      return;\n    }\n    const first = focusables[0];\n    const last = focusables[focusables.length - 1];\n    if (event.shiftKey && document.activeElement === first) {\n      event.preventDefault();\n      last.focus();\n    } else if (!event.shiftKey && document.activeElement === last) {\n      event.preventDefault();\n      first.focus();\n    } else if (!focusables.includes(document.activeElement as HTMLElement)) {\n      event.preventDefault();\n      (event.shiftKey ? last : first).focus();\n    }\n  }\n\n  useEffect(() => {\n    if (!open) return undefined;\n    const onPointerDown = (event: PointerEvent) => {\n      if (!rootRef.current?.contains(event.target as Node)) setOpen(false);\n    };\n    document.addEventListener(\"pointerdown\", onPointerDown);\n    return () => document.removeEventListener(\"pointerdown\", onPointerDown);\n  }, [open]);\n\n  useEffect(() => {\n    if (inspect && !open) {\n      freshRef.current = true;\n      pendingFocusRef.current = null;\n      setView({ name: \"share\" });\n      setOpen(true);\n    } else if (!inspect && open && !rootRef.current?.contains(document.activeElement)) {\n      setOpen(false);\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [inspect]);\n\n  useEffect(\n    () => () => {\n      clearTimeout(leaveTimerRef.current);\n      clearTimeout(copyTimerRef.current);\n    },\n    [],\n  );\n\n  useEffect(() => {\n    onStateChange?.({\n      open,\n      view: view.name === \"role\" ? `role · ${view.person.name}` : view.name,\n      people: people.length + 1,\n      link: scopeLabel(linkScope),\n      size: dims ? { w: Math.round(dims.w), h: Math.round(dims.h) } : null,\n      lastAction,\n    });\n  }, [open, view, people.length, linkScope, dims, lastAction, onStateChange]);\n\n  // ── Views (one renderer serves the live view and the leaving snapshot) ─\n  function renderView(target: View, live: boolean) {\n    if (target.name === \"role\") {\n      const person = target.person;\n      const current = roles[person.id];\n      return (\n        <div className=\"p-2 w-[15rem]\">\n          <button\n            type=\"button\"\n            className=\"flex items-center gap-1.5 w-full p-1.5 rounded-lg bg-transparent text-muted-foreground text-left cursor-pointer transition-colors duration-150 hover:bg-accent focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring\"\n            onClick={live ? () => backFromRole(person) : undefined}\n          >\n            <ChevronLeftIcon />\n            <span className=\"flex flex-col min-w-0\">\n              <span className=\"text-[0.8125rem] font-medium text-foreground\">{person.name}</span>\n              <span className=\"text-[0.6875rem] text-muted-foreground/70\">{person.email}</span>\n            </span>\n          </button>\n          <div className=\"flex flex-col mt-1\" role=\"group\" aria-label={`Role for ${person.name}`}>\n            {ROLES.map((role) => (\n              <button\n                key={role.id}\n                type=\"button\"\n                className={OPT}\n                data-role-opt={role.id}\n                aria-pressed={role.id === current}\n                onClick={live ? () => pickRole(person, role.id) : undefined}\n              >\n                <span className=\"flex flex-col min-w-0 flex-1\">\n                  <span className=\"text-[0.8125rem] text-foreground\">{role.label}</span>\n                  <span className=\"text-[0.6875rem] text-muted-foreground/70\">{role.hint}</span>\n                </span>\n                {role.id === current && (\n                  <span className=\"inline-flex flex-none text-foreground\">\n                    <CheckIcon />\n                  </span>\n                )}\n              </button>\n            ))}\n            <div className=\"h-px mx-1.5 my-1.5 bg-border\" role=\"presentation\" />\n            <button\n              type=\"button\"\n              className=\"group/danger flex items-center gap-2 w-full p-1.5 rounded-lg bg-transparent text-left cursor-pointer transition-colors duration-150 hover:bg-destructive/10 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring\"\n              onClick={live ? () => removePerson(person) : undefined}\n            >\n              <span className=\"flex flex-col min-w-0 flex-1\">\n                <span className=\"text-[0.8125rem] text-foreground group-hover/danger:text-destructive\">Remove access</span>\n                <span className=\"text-[0.6875rem] text-muted-foreground/70 group-hover/danger:text-destructive\">They lose this doc</span>\n              </span>\n            </button>\n          </div>\n        </div>\n      );\n    }\n\n    if (target.name === \"link\") {\n      return (\n        <div className=\"p-2 w-[17rem]\">\n          <button\n            type=\"button\"\n            className=\"flex items-center gap-1.5 w-full p-1.5 rounded-lg bg-transparent text-muted-foreground text-left cursor-pointer transition-colors duration-150 hover:bg-accent focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring\"\n            onClick={live ? backFromLink : undefined}\n          >\n            <ChevronLeftIcon />\n            <span className=\"flex flex-col min-w-0\">\n              <span className=\"text-[0.8125rem] font-medium text-foreground\">Link access</span>\n              <span className=\"text-[0.6875rem] text-muted-foreground/70\">Who can use the doc link</span>\n            </span>\n          </button>\n          <div className=\"flex flex-col mt-1\" role=\"group\" aria-label=\"Link access\">\n            {LINK_SCOPES.map((scope) => (\n              <button\n                key={scope.id}\n                type=\"button\"\n                className={OPT}\n                data-link-opt={scope.id}\n                aria-pressed={scope.id === linkScope}\n                onClick={live ? () => pickScope(scope.id) : undefined}\n              >\n                <span className=\"inline-flex flex-none text-muted-foreground/70\">\n                  <ScopeIcon name={scope.icon} />\n                </span>\n                <span className=\"flex flex-col min-w-0 flex-1\">\n                  <span className=\"text-[0.8125rem] text-foreground\">{scope.label}</span>\n                  <span className=\"text-[0.6875rem] text-muted-foreground/70\">{scope.hint}</span>\n                </span>\n                {scope.id === linkScope && (\n                  <span className=\"inline-flex flex-none text-foreground\">\n                    <CheckIcon />\n                  </span>\n                )}\n              </button>\n            ))}\n          </div>\n        </div>\n      );\n    }\n\n    return (\n      <div className=\"p-2 w-[19rem]\">\n        <div className=\"flex gap-1.5 mb-2\">\n          <input\n            data-invite-input\n            className=\"flex-1 min-w-0 h-8 px-2.5 rounded-lg bg-muted text-[0.8125rem] text-foreground outline-none transition-[background-color,box-shadow] duration-150 placeholder:text-muted-foreground/70 focus:bg-popover focus:shadow-[inset_0_0_0_1.5px_var(--color-ring)]\"\n            type=\"text\"\n            value={live ? invite : \"\"}\n            placeholder=\"Invite by name or email\"\n            aria-label=\"Invite by name or email\"\n            spellCheck={false}\n            onChange={live ? (event) => setInvite(event.target.value) : undefined}\n            onKeyDown={\n              live\n                ? (event) => {\n                    if (event.key === \"Enter\") {\n                      event.preventDefault();\n                      handleInvite();\n                    }\n                  }\n                : undefined\n            }\n            readOnly={!live}\n          />\n          <button\n            type=\"button\"\n            className=\"relative h-8 px-2.5 rounded-lg bg-primary text-primary-foreground text-xs font-medium cursor-pointer transition-[background-color,color,scale] duration-150 disabled:bg-muted disabled:text-muted-foreground/70 disabled:cursor-default active:enabled:scale-[0.96] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring after:content-[''] after:absolute after:inset-x-0 after:-inset-y-1\"\n            disabled={!live || parseInvite(invite) === null}\n            onClick={live ? handleInvite : undefined}\n          >\n            Invite\n          </button>\n        </div>\n\n        <p className=\"mx-1.5 mt-1 mb-0.5 text-[0.625rem] font-semibold tracking-[0.06em] uppercase text-muted-foreground/70\">\n          People with access\n        </p>\n        <div className=\"flex flex-col\">\n          <div className=\"flex items-center gap-2 px-1.5 py-[0.3125rem] rounded-lg\">\n            <span className=\"inline-flex items-center justify-center flex-none w-[1.625rem] h-[1.625rem] rounded-full bg-primary text-primary-foreground text-[0.5625rem] font-semibold tracking-[0.02em]\" aria-hidden=\"true\">\n              {initials(owner.name)}\n            </span>\n            <span className=\"flex flex-col min-w-0 flex-1\">\n              <span className=\"text-[0.8125rem] text-foreground overflow-hidden text-ellipsis whitespace-nowrap\">{owner.name}</span>\n              <span className=\"text-[0.6875rem] text-muted-foreground/70 overflow-hidden text-ellipsis whitespace-nowrap\">{owner.email}</span>\n            </span>\n            <span className=\"flex-none text-xs text-muted-foreground/70 pr-1\">Owner</span>\n          </div>\n          {people.map((person) => (\n            <div className=\"flex items-center gap-2 px-1.5 py-[0.3125rem] rounded-lg\" key={person.id}>\n              <span className=\"inline-flex items-center justify-center flex-none w-[1.625rem] h-[1.625rem] rounded-full bg-foreground/10 text-foreground/70 text-[0.5625rem] font-semibold tracking-[0.02em]\" aria-hidden=\"true\">\n                {initials(person.name)}\n              </span>\n              <span className=\"flex flex-col min-w-0 flex-1\">\n                <span className=\"text-[0.8125rem] text-foreground overflow-hidden text-ellipsis whitespace-nowrap\">{person.name}</span>\n                <span className=\"text-[0.6875rem] text-muted-foreground/70 overflow-hidden text-ellipsis whitespace-nowrap\">{person.email}</span>\n              </span>\n              <button\n                type=\"button\"\n                className=\"relative inline-flex items-center gap-1 flex-none h-8 px-2 rounded-md bg-transparent text-muted-foreground text-xs cursor-pointer transition-[background-color,color,scale] duration-150 hover:bg-accent hover:text-foreground active:scale-[0.96] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring after:content-[''] after:absolute after:inset-x-0 after:-inset-y-1\"\n                data-person={person.id}\n                aria-label={`${person.name}: ${roleLabel(roles[person.id])}. Change role`}\n                onClick={live ? () => goRole(person) : undefined}\n              >\n                {roleLabel(roles[person.id])}\n                <ChevronRightIcon />\n              </button>\n            </div>\n          ))}\n        </div>\n\n        <div className=\"h-px mx-1.5 my-1.5 bg-border\" role=\"presentation\" />\n        <button\n          type=\"button\"\n          className=\"flex items-center gap-2 w-full p-1.5 rounded-lg bg-transparent text-muted-foreground/70 text-left cursor-pointer transition-colors duration-150 hover:bg-accent focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring\"\n          data-link-row\n          onClick={live ? goLink : undefined}\n        >\n          <span className=\"inline-flex flex-none text-muted-foreground/70\">\n            <ScopeIcon name={LINK_SCOPES.find((s) => s.id === linkScope)!.icon} />\n          </span>\n          <span className=\"flex flex-col min-w-0 flex-1\">\n            <span className=\"text-[0.8125rem] text-foreground overflow-hidden text-ellipsis whitespace-nowrap\">Link access</span>\n            <span className=\"text-[0.6875rem] text-muted-foreground/70 overflow-hidden text-ellipsis whitespace-nowrap\">{scopeLabel(linkScope)}</span>\n          </span>\n          <ChevronRightIcon />\n        </button>\n\n        <div className=\"flex items-center justify-between gap-2 mt-1.5 pt-1.5 px-1.5 pb-0.5 border-t border-border\">\n          <button\n            type=\"button\"\n            className=\"relative inline-flex items-center gap-1.5 h-8 pl-7 pr-2 rounded-lg bg-transparent text-muted-foreground text-xs font-medium cursor-pointer transition-[background-color,color,scale] duration-150 hover:bg-accent hover:text-foreground active:scale-[0.96] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring after:content-[''] after:absolute after:inset-x-0 after:-inset-y-1\"\n            onClick={live ? copyLink : undefined}\n          >\n            <motion.span\n              className=\"absolute left-2 top-1/2 inline-flex\"\n              style={{ y: \"-50%\" }}\n              initial={false}\n              animate={copied && live ? { opacity: 0, scale: 0.25, filter: \"blur(4px)\" } : { opacity: 1, scale: 1, filter: \"blur(0px)\" }}\n              transition={{ duration: 0.25, ease: EASE_ICON }}\n            >\n              <LinkIcon />\n            </motion.span>\n            <motion.span\n              className=\"absolute left-2 top-1/2 inline-flex text-[#16a34a]\"\n              style={{ y: \"-50%\" }}\n              aria-hidden=\"true\"\n              initial={false}\n              animate={copied && live ? { opacity: 1, scale: 1, filter: \"blur(0px)\" } : { opacity: 0, scale: 0.25, filter: \"blur(4px)\" }}\n              transition={{ duration: 0.25, ease: EASE_ICON }}\n            >\n              <CheckIcon />\n            </motion.span>\n            {copied && live ? \"Copied\" : \"Copy link\"}\n          </button>\n          <span className=\"text-[0.6875rem] text-muted-foreground/70 tabular-nums\">{people.length + 1} people</span>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <div ref={rootRef} className=\"relative w-full max-w-96\" data-inspect={inspect ? \"true\" : \"false\"}>\n        {/* The anchor context: a fake doc header the popover hangs off. */}\n        <div className=\"flex items-center gap-2.5 px-3.5 py-3 bg-card rounded-xl shadow-border\">\n          <span className=\"inline-flex flex-none text-muted-foreground/70\" aria-hidden=\"true\">\n            <DocIcon />\n          </span>\n          <span className=\"flex flex-col min-w-0 flex-1\">\n            <span className=\"text-sm font-medium text-foreground\">{docTitle}</span>\n            <span className=\"text-[0.6875rem] text-muted-foreground/70\">Edited 2h ago</span>\n          </span>\n          <button\n            ref={triggerRef}\n            type=\"button\"\n            className={`relative inline-flex items-center gap-1.5 h-8 px-3 rounded-lg bg-primary text-primary-foreground text-[0.8125rem] font-medium cursor-pointer transition-[background-color,scale] duration-150 hover:bg-primary/85 active:scale-[0.96] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring after:content-[''] after:absolute after:inset-x-0 after:-inset-y-1${\n              inspect ? \" outline outline-[1.5px] outline-dashed outline-[#ef4444] outline-offset-[3px]\" : \"\"\n            }`}\n            aria-haspopup=\"dialog\"\n            aria-expanded={open}\n            onClick={() => (open ? closePop(false) : openPop())}\n          >\n            <ShareIcon />\n            Share\n          </button>\n        </div>\n\n        {/* Always mounted so open/close are interruptible; inert while closed. */}\n        <motion.div\n          className=\"absolute top-[calc(100%+0.5rem)] right-0 z-20 [transform-origin:calc(100%-2.25rem)_-0.375rem]\"\n          initial={false}\n          animate={open ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.97 }}\n          transition={{ duration: morph && !reduced ? (open ? 0.18 : 0.16) : 0, ease: EASE }}\n          style={{ pointerEvents: open ? \"auto\" : \"none\" }}\n          role=\"dialog\"\n          aria-modal={open && trap ? \"true\" : undefined}\n          aria-label={`Share ${docTitle}`}\n          aria-hidden={!open}\n          inert={!open}\n          onKeyDown={handleTrapKeys}\n        >\n          <motion.div\n            ref={frameRef}\n            className={`relative overflow-hidden bg-popover rounded-xl shadow-[var(--shadow-border),0_12px_32px_-12px_rgba(0,0,0,0.18)]${\n              inspect ? \" outline outline-[1.5px] outline-dashed outline-[#3b82f6] outline-offset-[3px]\" : \"\"\n            }`}\n            animate={dims ? { width: dims.w, height: dims.h } : undefined}\n            transition={!morph || reduced || snapRef.current ? { duration: 0 } : { duration: 0.3, ease: EASE }}\n          >\n            {leaving && morph && !reduced && (\n              <motion.div\n                className=\"absolute top-0 left-0 w-max pointer-events-none\"\n                initial={{ x: 0, opacity: 1 }}\n                animate={{ x: -leaving.dir * SLIDE, opacity: 0 }}\n                transition={{ duration: 0.16, ease: \"easeOut\" }}\n                aria-hidden=\"true\"\n                inert\n              >\n                {renderView(leaving.view, false)}\n              </motion.div>\n            )}\n            <motion.div\n              className=\"absolute top-0 left-0 w-max\"\n              key={viewKey}\n              ref={viewRef}\n              initial={leaving && morph && !reduced ? { x: leaving.dir * SLIDE, opacity: 0, filter: \"blur(2px)\" } : false}\n              animate={{ x: 0, opacity: 1, filter: \"blur(0px)\" }}\n              transition={{ duration: 0.3, ease: EASE }}\n            >\n              {renderView(view, true)}\n            </motion.div>\n          </motion.div>\n        </motion.div>\n\n        {lastAction && (\n          <span className=\"sr-only\" aria-live=\"polite\">\n            {lastAction}\n          </span>\n        )}\n\n        {inspect && open && (\n          <>\n            <span className=\"absolute bottom-[calc(100%+0.4rem)] left-0 z-[25] whitespace-nowrap rounded-[0.25rem] border border-[#bfdbfe] bg-white px-[0.3125rem] py-[0.0625rem] text-[0.625rem] font-medium leading-normal tracking-[0.01em] text-[#2563eb] shadow-[0_1px_2px_rgba(0,0,0,0.08)] pointer-events-none tabular-nums\">\n              size: {dims ? `${Math.round(dims.w)} × ${Math.round(dims.h)}px` : \"measured\"} · both axes eased\n            </span>\n            <span className=\"absolute bottom-[calc(100%+0.4rem)] right-0 z-[25] whitespace-nowrap rounded-[0.25rem] border border-[#fecaca] bg-white px-[0.3125rem] py-[0.0625rem] text-[0.625rem] font-medium leading-normal tracking-[0.01em] text-[#dc2626] shadow-[0_1px_2px_rgba(0,0,0,0.08)] pointer-events-none tabular-nums\">\n              {trap ? \"trap: Tab cycles the live view · Esc backs out\" : \"trap OFF · Tab walks out of the popover\"}\n            </span>\n            <span className=\"absolute top-[3.25rem] right-0 z-[25] whitespace-nowrap rounded-[0.25rem] border border-[#fecaca] bg-white px-[0.3125rem] py-[0.0625rem] text-[0.625rem] font-medium leading-normal tracking-[0.01em] text-[#dc2626] shadow-[0_1px_2px_rgba(0,0,0,0.08)] pointer-events-none tabular-nums\">\n              origin: under the trigger · scale 0.96 → 1\n            </span>\n          </>\n        )}\n      </div>\n    </MotionConfig>\n  );\n}\n\nconst INITIAL_PEOPLE: Person[] = [\n  { id: \"sarah\", name: \"Sarah Chen\", email: \"sarah@acme.co\" },\n  { id: \"omar\", name: \"Omar Farouk\", email: \"omar@acme.co\" },\n  { id: \"june\", name: \"June Park\", email: \"june@acme.co\" },\n];\nconst INITIAL_ROLES: Record<string, string> = { sarah: \"full\", omar: \"edit\", june: \"view\" };\n\nfunction Svg({ children, size = 15, sw = 1.8 }: { children: React.ReactNode; size?: number; sw?: number }) {\n  return (\n    <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={sw} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n      {children}\n    </svg>\n  );\n}\n\nfunction ScopeIcon({ name }: { name: \"globe\" | \"users\" | \"lock\" }) {\n  if (name === \"globe\") return <Svg><circle cx=\"12\" cy=\"12\" r=\"10\" /><path d=\"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20M2 12h20\" /></Svg>;\n  if (name === \"users\") return <Svg><path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\" /><circle cx=\"9\" cy=\"7\" r=\"4\" /><path d=\"M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75\" /></Svg>;\n  return <Svg><rect x=\"3\" y=\"11\" width=\"18\" height=\"11\" rx=\"2\" /><path d=\"M7 11V7a5 5 0 0 1 10 0v4\" /></Svg>;\n}\nfunction ShareIcon() { return <Svg size={13} sw={2}><path d=\"M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8\" /><path d=\"m16 6-4-4-4 4M12 2v13\" /></Svg>; }\nfunction DocIcon() { return <Svg size={16}><path d=\"M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5Z\" /><path d=\"M14 2v4a2 2 0 0 0 2 2h4M8 13h8M8 17h5\" /></Svg>; }\nfunction LinkIcon() { return <Svg size={13}><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\" /><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\" /></Svg>; }\nfunction ChevronLeftIcon() { return <Svg size={14} sw={2}><path d=\"m15 18-6-6 6-6\" /></Svg>; }\nfunction ChevronRightIcon() { return <Svg size={12} sw={2}><path d=\"m9 18 6-6-6-6\" /></Svg>; }\nfunction CheckIcon() { return <Svg size={13} sw={2.5}><path d=\"M20 6 9 17l-5-5\" /></Svg>; }\n"
    }
  ]
}
