{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "type": "registry:ui",
  "slug": "access-request",
  "name": "Access Request",
  "title": "Access Request",
  "description": "A compact avatar pill that unfolds into access requests with shared avatar motion, individual approve and decline actions, and coordinated spring reflow.",
  "summary": "An inline request-review notification based on the supplied motion reference. Shared avatars move into their rows as the pill unfolds; decisions show pending and outcome states before rows leave and the queue settles. Supports live data, retries, focus recovery, cancellation, both themes and reduced motion.",
  "category": "feedback",
  "tags": [
    "access",
    "request",
    "approval",
    "decline",
    "permissions",
    "morph",
    "avatars",
    "gsap",
    "spring"
  ],
  "dependencies": [
    "gsap",
    "@gsap/react",
    "motion",
    "lucide-react",
    "@radix-ui/react-avatar",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "avatar"
  ],
  "version": "1.0.0",
  "createdDate": "2026-10-19",
  "updatedDate": "2026-10-19",
  "interactive": true,
  "supportsColor": false,
  "highlights": [
    "Shared avatar elements travel between the compact stack and expanded rows",
    "Interruptible spring-driven panel height, header crossfade, count badge and chevron",
    "Independent asynchronous decisions with duplicate-click guards and AbortSignal cancellation",
    "Successful rows acknowledge the decision before exiting; remaining rows and avatars reflow together",
    "Retryable inline errors and live queue updates preserve unresolved requests",
    "Light and dark themes, keyboard focus recovery, inert collapsed content and reduced motion",
    "Transparent component-only showcase with adjustable width, stiffness, damping and speed"
  ],
  "anatomy": [
    "Named region with a compact review trigger, disclosure control and dismiss button",
    "Shared avatar layer with an overflow count",
    "Inert collapsible panel containing a semantic request list",
    "Each row contains requester details, approve/decline controls and an optional retry alert",
    "Polite action announcements and an empty queue summary"
  ],
  "physics": {
    "engine": "GSAP + spring easing",
    "description": "One scoped timeline coordinates measured panel height, the shared avatar layer, disclosure icon, header and row positions. New transitions start from current values. Each decision has a separate scoped outcome and exit sequence.",
    "parameters": [
      {
        "label": "Stiffness",
        "value": "420 (80–600)"
      },
      {
        "label": "Damping",
        "value": "28 (16–60)"
      },
      {
        "label": "Speed",
        "value": "1× (0.5–2)"
      },
      {
        "label": "Decision acknowledgement",
        "value": "250ms"
      },
      {
        "label": "Row/widget exit",
        "value": "180ms"
      }
    ]
  },
  "accessibility": {
    "role": "region",
    "aria": "Named region and disclosure controls with aria-expanded/aria-controls. Collapsed content is inert and hidden to assistive technology. Decisions are labelled with requester names, pending rows expose aria-busy, safe failures use alerts, and outcomes are announced politely.",
    "reducedMotion": "Panel, avatar, row and exit changes apply immediately; pending indicators remain static and all controls stay usable.",
    "keyboard": [
      {
        "key": "Tab / Shift+Tab",
        "description": "Move among disclosure, dismissal and per-person actions."
      },
      {
        "key": "Enter / Space",
        "description": "Open, collapse, approve, decline or dismiss the focused control."
      },
      {
        "key": "Escape",
        "description": "Collapse the expanded panel and focus its toggle; dismiss the widget when already collapsed."
      }
    ]
  },
  "guidelines": {
    "recommended": [
      "Workspace and document access queues",
      "Invitations awaiting review",
      "Compact administrative approval notifications"
    ],
    "bestPractices": [
      "Use a distinct request ID for each access request, even when the same person requests access again.",
      "Authorize and persist decisions on your server; this component handles presentation and callback state.",
      "Remove successful items in onResolved, after their exit has finished.",
      "Honor AbortSignal and make decision endpoints idempotent; cancelling the UI cannot roll back a decision already committed by a server.",
      "Keep names concise and supply safe descriptions and error messages.",
      "The inline panel does not trap focus or automatically open on hover.",
      "The showcase only simulates local decisions and does not change real access permissions."
    ]
  },
  "props": [
    {
      "name": "requests",
      "type": "readonly AccessRequester[]",
      "required": true,
      "description": "Live queue of requesters with unique, stable IDs, names and optional handle, avatar URL, emoji fallback, color and description. Newly received requests join the queue; externally removed requests animate out."
    },
    {
      "name": "onDecision",
      "type": "(person: AccessRequester, decision: AccessDecision, signal: AbortSignal) => void | Promise<void>",
      "required": true,
      "description": "Commit an approved or declined decision. Resolve only after success; reject to retain the row with a safe retry message. Honor AbortSignal to cancel work on removal, dismissal or unmount."
    },
    {
      "name": "onResolved",
      "type": "(person: AccessRequester, decision: AccessDecision) => void",
      "description": "Runs after the successful row exit. Use this callback to remove the request from parent state without interrupting its animation."
    },
    {
      "name": "onDismiss",
      "type": "() => void",
      "description": "Runs after the complete widget exit. Dismissal cancels pending callbacks; it does not decline the queue. Remount to display the widget again."
    },
    {
      "name": "expanded",
      "type": "boolean",
      "description": "Optional controlled disclosure state. Pair with onExpandedChange."
    },
    {
      "name": "defaultExpanded",
      "type": "boolean",
      "defaultValue": "false",
      "description": "Initial disclosure state in uncontrolled mode."
    },
    {
      "name": "onExpandedChange",
      "type": "(expanded: boolean) => void",
      "description": "Receives toggle and Escape collapse requests."
    },
    {
      "name": "disabled",
      "type": "boolean",
      "defaultValue": "false",
      "description": "Disable decisions while preserving disclosure and dismissal controls."
    },
    {
      "name": "width",
      "type": "number",
      "defaultValue": "360",
      "description": "Preferred width, clamped to 280–480px and constrained by the parent."
    },
    {
      "name": "stiffness",
      "type": "number",
      "defaultValue": "420",
      "description": "Spring tension, clamped to 80–600."
    },
    {
      "name": "damping",
      "type": "number",
      "defaultValue": "28",
      "description": "Spring friction, clamped to 16–60."
    },
    {
      "name": "speed",
      "type": "number",
      "defaultValue": "1",
      "description": "Playback multiplier, clamped to 0.5–2."
    },
    {
      "name": "errorMessage",
      "type": "string",
      "defaultValue": "\"Couldn’t save this decision. Please try again.\"",
      "description": "Safe user-facing text for failed actions. Server errors are not rendered."
    },
    {
      "name": "className",
      "type": "string",
      "description": "Additional classes for the notification shell."
    },
    {
      "name": "ref",
      "type": "Ref<HTMLDivElement>",
      "description": "Receives the mounted notification region."
    }
  ],
  "usage": "\"use client\";\n\nimport { useState } from \"react\";\nimport { AccessRequest, type AccessRequester } from \"@/components/ui/access-request\";\n\nexport function RequestReview({ initialRequests }: { initialRequests: AccessRequester[] }) {\n  const [requests, setRequests] = useState(initialRequests);\n\n  return (\n    <AccessRequest\n      requests={requests}\n      onDecision={async (person, decision, signal) => {\n        const response = await fetch(`/api/access-requests/${encodeURIComponent(person.id)}`, {\n          method: \"PATCH\",\n          headers: { \"Content-Type\": \"application/json\" },\n          body: JSON.stringify({ decision }),\n          signal,\n        });\n        if (!response.ok) throw new Error(\"Decision was not saved\");\n      }}\n      onResolved={(person) => setRequests((current) => current.filter((item) => item.id !== person.id))}\n      stiffness={420}\n      damping={28}\n    />\n  );\n}",
  "files": [
    {
      "name": "access-request.tsx",
      "path": "registry/ui/access-request.tsx",
      "type": "registry:ui",
      "code": "\"use client\";\n\nimport {\n  useEffect,\n  useId,\n  useReducer,\n  useRef,\n  useState,\n  type Ref,\n} from \"react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { Check, ChevronDown, ShieldCheck, X } from \"lucide-react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport { useMediaQuery } from \"@/lib/use-media-query\";\nimport { cn } from \"@/lib/utils\";\nimport { AccessRequestRow } from \"./access-request-row\";\nimport {\n  accessRequestReducer,\n  createAccessRequestState,\n  type AccessDecision,\n  type AccessRequester,\n  type AccessRequestEntry,\n} from \"./access-request-state\";\nimport { useAccessRequestMotion } from \"./use-access-request-motion\";\n\nexport type { AccessDecision, AccessRequester } from \"./access-request-state\";\n\ngsap.registerPlugin(useGSAP);\n\nexport interface AccessRequestProps {\n  requests: readonly AccessRequester[];\n  onDecision: (\n    person: AccessRequester,\n    decision: AccessDecision,\n    signal: AbortSignal,\n  ) => void | Promise<void>;\n  onResolved?: (person: AccessRequester, decision: AccessDecision) => void;\n  onDismiss?: () => void;\n  expanded?: boolean;\n  defaultExpanded?: boolean;\n  onExpandedChange?: (expanded: boolean) => void;\n  disabled?: boolean;\n  width?: number;\n  stiffness?: number;\n  damping?: number;\n  speed?: number;\n  errorMessage?: string;\n  className?: string;\n  ref?: Ref<HTMLDivElement>;\n}\n\nconst controlClass =\n  \"flex size-8 cursor-pointer items-center justify-center rounded-lg text-zinc-500 outline-none hover:bg-zinc-100 hover:text-zinc-900 focus-visible:ring-2 focus-visible:ring-zinc-500 disabled:pointer-events-none pointer-coarse:size-10 dark:text-zinc-400 dark:hover:bg-white/5 dark:hover:text-white dark:focus-visible:ring-zinc-400\";\nconst colors = [\"#ede9fe\", \"#ffe4e6\", \"#fef9c3\", \"#dcfce7\"];\n\nfunction RequestAvatar({\n  person,\n  index,\n  expanded,\n  count,\n}: {\n  person: AccessRequester;\n  index: number;\n  expanded: boolean;\n  count: number;\n}) {\n  const [initialIndex] = useState(index);\n  const colorIndex =\n    Array.from(person.id).reduce(\n      (total, letter) => total + letter.charCodeAt(0),\n      0,\n    ) % colors.length;\n  return (\n    <div\n      data-request-avatar={person.id}\n      className=\"absolute top-0 left-0\"\n      style={{\n        transform: `translate(${expanded ? 16 : 16 + initialIndex * 21}px, ${expanded ? 81 + initialIndex * 56 : 16}px)`,\n        opacity: expanded || initialIndex < 2 ? 1 : 0,\n        zIndex: count - index + 1,\n      }}\n    >\n      <Avatar className=\"size-8 border-2 border-white dark:border-[#171717]\">\n        {person.avatar && (\n          <AvatarImage src={person.avatar} alt=\"\" className=\"object-cover\" />\n        )}\n        <AvatarFallback\n          className={cn(\n            \"text-[11px] font-semibold text-zinc-800\",\n            person.emoji && \"text-[20px]\",\n          )}\n          style={{ backgroundColor: person.color ?? colors[colorIndex] }}\n        >\n          {person.emoji ??\n            person.name\n              .split(/\\s+/)\n              .filter(Boolean)\n              .slice(0, 2)\n              .map((word) => word[0])\n              .join(\"\")}\n        </AvatarFallback>\n      </Avatar>\n    </div>\n  );\n}\n\nexport function AccessRequest({\n  requests,\n  onDecision,\n  onResolved,\n  onDismiss,\n  expanded,\n  defaultExpanded = false,\n  onExpandedChange,\n  disabled = false,\n  width = 360,\n  stiffness = 420,\n  damping = 28,\n  speed = 1,\n  errorMessage = \"Couldn’t save this decision. Please try again.\",\n  className,\n  ref,\n}: AccessRequestProps) {\n  const [state, dispatch] = useReducer(\n    accessRequestReducer,\n    requests,\n    createAccessRequestState,\n  );\n  const [ownExpanded, setOwnExpanded] = useState(defaultExpanded);\n  const [dismissing, setDismissing] = useState(false);\n  const [dismissed, setDismissed] = useState(false);\n  const [announcement, setAnnouncement] = useState(\"\");\n  const [initialExpanded] = useState(expanded ?? defaultExpanded);\n  const [initialCount] = useState(state.entries.length);\n  const rootRef = useRef<HTMLDivElement>(null);\n  const panelRef = useRef<HTMLDivElement>(null);\n  const contentRef = useRef<HTMLDivElement>(null);\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const pendingRef = useRef(new Map<string, AbortController>());\n  const decisionFocusRef = useRef(new Set<string>());\n  const nextFocusRef = useRef<number | null>(null);\n  const callbacksRef = useRef({ onResolved, onDismiss });\n  const dismissingRef = useRef(false);\n  const uid = useId();\n  const isOpen = expanded ?? ownExpanded;\n  const reduced = useMediaQuery(\"(prefers-reduced-motion: reduce)\");\n  const count = state.entries.length;\n  const pendingCount = state.entries.filter(\n    (entry) => entry.phase === \"pending\",\n  ).length;\n  const reviewed = state.approved + state.declined;\n  const first = state.entries[0]?.person;\n  const summary = first\n    ? `${first.handle ? `@${first.handle.replace(/^@/, \"\")}` : first.name}${count > 1 ? ` and ${count - 1} ${count === 2 ? \"other\" : \"others\"}` : \"\"}`\n    : \"All caught up\";\n  const signature = JSON.stringify(\n    state.entries.map((entry) => [entry.person.id, entry.phase, entry.error]),\n  );\n  const size = Number.isFinite(width)\n    ? Math.min(480, Math.max(280, width))\n    : 360;\n\n  useAccessRequestMotion({\n    rootRef,\n    panelRef,\n    contentRef,\n    signature,\n    expanded: isOpen,\n    stiffness,\n    damping,\n    speed,\n    enabled: !dismissed,\n  });\n  const { contextSafe } = useGSAP({ scope: rootRef });\n\n  useGSAP(\n    () => {\n      callbacksRef.current = { onResolved, onDismiss };\n    },\n    { dependencies: [onResolved, onDismiss] },\n  );\n\n  useEffect(() => {\n    dispatch({ type: \"sync\", requests });\n  }, [requests]);\n\n  useEffect(() => {\n    for (const [id, controller] of pendingRef.current) {\n      const entry = state.entries.find((item) => item.person.id === id);\n      if (!entry || entry.phase === \"removing\") {\n        controller.abort();\n        pendingRef.current.delete(id);\n      }\n    }\n  }, [state.entries]);\n\n  useEffect(() => {\n    const pending = pendingRef.current;\n    return () => {\n      for (const controller of pending.values()) controller.abort();\n      pending.clear();\n    };\n  }, []);\n\n  useGSAP(\n    () => {\n      if (nextFocusRef.current === null) return;\n      const rows = Array.from(\n        contentRef.current?.querySelectorAll<HTMLElement>(\n          \"[data-request-row]\",\n        ) ?? [],\n      );\n      const row = rows[Math.min(nextFocusRef.current, rows.length - 1)];\n      (isOpen\n        ? (row?.querySelector<HTMLButtonElement>(\"button:not(:disabled)\") ??\n          triggerRef.current)\n        : triggerRef.current\n      )?.focus({ preventScroll: true });\n      nextFocusRef.current = null;\n    },\n    { scope: rootRef, dependencies: [signature, isOpen] },\n  );\n\n  const changeExpanded = (next: boolean) => {\n    if (dismissingRef.current) return;\n    if (expanded === undefined) setOwnExpanded(next);\n    onExpandedChange?.(next);\n    if (!next) triggerRef.current?.focus({ preventScroll: true });\n  };\n\n  const decide = async (\n    entry: AccessRequestEntry,\n    decision: AccessDecision,\n  ) => {\n    const { person } = entry;\n    if (\n      disabled ||\n      dismissingRef.current ||\n      entry.phase !== \"idle\" ||\n      pendingRef.current.has(person.id)\n    )\n      return;\n    const controller = new AbortController();\n    pendingRef.current.set(person.id, controller);\n    const row = Array.from(\n      contentRef.current?.querySelectorAll<HTMLElement>(\"[data-request-row]\") ??\n        [],\n    ).find((element) => element.dataset.requestRow === person.id);\n    if (row?.contains(document.activeElement))\n      decisionFocusRef.current.add(person.id);\n    dispatch({ type: \"begin\", id: person.id, decision });\n    setAnnouncement(\n      `${decision === \"approved\" ? \"Approving\" : \"Declining\"} ${person.name}.`,\n    );\n    try {\n      await onDecision(person, decision, controller.signal);\n      if (controller.signal.aborted) return;\n      dispatch({ type: \"resolve\", id: person.id, decision });\n      setAnnouncement(`${person.name} ${decision}.`);\n    } catch {\n      if (!controller.signal.aborted) {\n        dispatch({ type: \"reject\", id: person.id, message: errorMessage });\n        setAnnouncement(\n          `Decision for ${person.name} failed. Please try again.`,\n        );\n        if (\n          decisionFocusRef.current.has(person.id) &&\n          document.activeElement === document.body\n        )\n          nextFocusRef.current = state.entries.findIndex(\n            (item) => item.person.id === person.id,\n          );\n      }\n    } finally {\n      if (pendingRef.current.get(person.id) === controller)\n        pendingRef.current.delete(person.id);\n    }\n  };\n\n  const finish = (entry: AccessRequestEntry) => {\n    const rows = Array.from(\n      contentRef.current?.querySelectorAll<HTMLElement>(\"[data-request-row]\") ??\n        [],\n    );\n    const row = rows.find(\n      (element) => element.dataset.requestRow === entry.person.id,\n    );\n    if (\n      row?.contains(document.activeElement) ||\n      (decisionFocusRef.current.has(entry.person.id) &&\n        document.activeElement === document.body)\n    )\n      nextFocusRef.current = state.entries.findIndex(\n        (item) => item.person.id === entry.person.id,\n      );\n    decisionFocusRef.current.delete(entry.person.id);\n    dispatch({ type: \"remove\", id: entry.person.id });\n    if (entry.phase === \"decided\" && entry.decision)\n      callbacksRef.current.onResolved?.(entry.person, entry.decision);\n  };\n\n  const dismiss = () =>\n    contextSafe(() => {\n      if (dismissingRef.current) return;\n      dismissingRef.current = true;\n      setDismissing(true);\n      for (const [id, controller] of pendingRef.current) {\n        controller.abort();\n        dispatch({ type: \"reject\", id, message: \"Decision cancelled.\" });\n      }\n      pendingRef.current.clear();\n      gsap.to(rootRef.current, {\n        autoAlpha: 0,\n        y: reduced ? 0 : -8,\n        scale: reduced ? 1 : 0.97,\n        duration: reduced ? 0 : 0.18,\n        ease: \"power2.in\",\n        overwrite: \"auto\",\n        onComplete: () => {\n          setDismissed(true);\n          callbacksRef.current.onDismiss?.();\n        },\n      });\n    })();\n\n  if (dismissed) return null;\n\n  return (\n    <div\n      ref={(node) => {\n        rootRef.current = node;\n        if (typeof ref === \"function\") ref(node);\n        else if (ref) ref.current = node;\n      }}\n      data-slot=\"access-request\"\n      data-expanded={isOpen}\n      role=\"region\"\n      aria-labelledby={`${uid}-heading`}\n      className={cn(\n        \"relative max-w-full overflow-hidden rounded-[20px] border border-zinc-200 bg-white text-zinc-900 shadow-[0_8px_30px_-12px_#00000020] dark:border-white/[0.07] dark:bg-[#171717] dark:text-zinc-100 dark:shadow-[0_8px_30px_-12px_#00000050]\",\n        className,\n      )}\n      style={{ width: size }}\n      onKeyDown={(event) => {\n        if (event.key === \"Escape\") {\n          event.preventDefault();\n          event.stopPropagation();\n          if (isOpen) changeExpanded(false);\n          else dismiss();\n        }\n      }}\n    >\n      <div className=\"relative h-16\">\n        <button\n          type=\"button\"\n          data-request-compact\n          aria-expanded={isOpen}\n          aria-controls={`${uid}-panel`}\n          aria-label={\n            count\n              ? `Review ${count} access ${count === 1 ? \"request\" : \"requests\"}`\n              : \"Review completed requests\"\n          }\n          aria-hidden={isOpen}\n          inert={isOpen || dismissing}\n          tabIndex={isOpen ? -1 : 0}\n          onClick={() => changeExpanded(true)}\n          className=\"absolute inset-y-0 left-0 right-20 cursor-pointer rounded-l-[20px] text-left outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-zinc-400\"\n          style={{ opacity: initialExpanded ? 0 : 1 }}\n        >\n          <span\n            data-request-summary\n            className=\"absolute inset-y-0 left-0 flex min-w-0 flex-col justify-center\"\n            style={{\n              transform: `translateX(${initialCount > 2 ? 108 : initialCount > 1 ? 84 : initialCount ? 60 : 58}px)`,\n            }}\n          >\n            <span className=\"truncate text-[12px] leading-5 font-medium\">\n              {summary}\n            </span>\n            <span className=\"truncate text-[11px] leading-4 text-zinc-500 dark:text-zinc-400\">\n              {pendingCount\n                ? `Reviewing ${pendingCount} ${pendingCount === 1 ? \"request\" : \"requests\"}…`\n                : count\n                  ? \"requested access\"\n                  : `${reviewed} ${reviewed === 1 ? \"request\" : \"requests\"} reviewed`}\n            </span>\n          </span>\n        </button>\n        <h3\n          id={`${uid}-heading`}\n          data-request-heading\n          className=\"pointer-events-none absolute top-[22px] left-4 text-[13px] leading-5 font-medium\"\n          style={{ opacity: initialExpanded ? 1 : 0 }}\n        >\n          Access requests\n        </h3>\n        <div className=\"absolute top-4 right-11 pointer-coarse:top-3\">\n          <button\n            ref={triggerRef}\n            type=\"button\"\n            aria-label={\n              isOpen ? \"Collapse access requests\" : \"Expand access requests\"\n            }\n            aria-expanded={isOpen}\n            aria-controls={`${uid}-panel`}\n            disabled={dismissing}\n            onClick={() => changeExpanded(!isOpen)}\n            className={controlClass}\n          >\n            <ChevronDown\n              data-request-chevron\n              size={17}\n              aria-hidden=\"true\"\n              style={{\n                transform: initialExpanded ? \"rotate(180deg)\" : undefined,\n              }}\n            />\n          </button>\n        </div>\n        <div className=\"absolute top-4 right-2 pointer-coarse:top-3\">\n          <button\n            type=\"button\"\n            aria-label=\"Dismiss access requests\"\n            onClick={dismiss}\n            disabled={dismissing}\n            className={controlClass}\n          >\n            <X size={17} aria-hidden=\"true\" />\n          </button>\n        </div>\n      </div>\n      <div\n        ref={panelRef}\n        id={`${uid}-panel`}\n        inert={!isOpen || dismissing}\n        aria-hidden={!isOpen}\n        className=\"overflow-hidden\"\n        style={{ height: initialExpanded ? \"auto\" : 0 }}\n      >\n        <div ref={contentRef} style={{ opacity: initialExpanded ? 1 : 0 }}>\n          <div className=\"mx-4 h-px bg-zinc-100 dark:bg-white/[0.07]\" />\n          {count ? (\n            <ul className=\"relative py-2\">\n              {state.entries.map((entry) => (\n                <AccessRequestRow\n                  key={entry.person.id}\n                  entry={entry}\n                  expanded={isOpen}\n                  disabled={disabled || dismissing}\n                  reduced={reduced}\n                  onDecision={(decision) => void decide(entry, decision)}\n                  onExit={() => finish(entry)}\n                />\n              ))}\n            </ul>\n          ) : (\n            <div className=\"flex flex-col items-center px-5 py-8 text-center\">\n              <span className=\"mb-3 flex size-10 items-center justify-center rounded-full bg-emerald-50 text-emerald-700 dark:bg-emerald-400/10 dark:text-emerald-400\">\n                <ShieldCheck size={20} aria-hidden=\"true\" />\n              </span>\n              <p className=\"text-[13px] font-medium\">\n                {reviewed\n                  ? \"Everyone’s accounted for.\"\n                  : \"No pending requests.\"}\n              </p>\n              <p className=\"mt-1.5 text-[11px] leading-5 text-zinc-500 dark:text-zinc-400\">\n                {reviewed\n                  ? `${state.approved} approved · ${state.declined} declined`\n                  : \"New access requests will appear here.\"}\n              </p>\n            </div>\n          )}\n        </div>\n      </div>\n      <div aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0\">\n        {state.entries.map(({ person }, index) => (\n          <RequestAvatar\n            key={person.id}\n            person={person}\n            index={index}\n            expanded={initialExpanded}\n            count={state.entries.length}\n          />\n        ))}\n        <span\n          data-request-overflow\n          className=\"absolute top-0 left-0 flex size-8 items-center justify-center rounded-full bg-zinc-100 text-[12px] font-medium text-zinc-600 dark:bg-[#272727] dark:text-zinc-300\"\n          style={{\n            transform: \"translate(58px, 16px)\",\n            opacity: !initialExpanded && initialCount > 2 ? 1 : 0,\n          }}\n        >\n          {Math.max(0, count - 2)}\n        </span>\n        {!count && (\n          <span\n            className=\"absolute top-4 left-4 flex size-8 items-center justify-center rounded-full bg-emerald-50 text-emerald-700 dark:bg-emerald-400/10 dark:text-emerald-400\"\n            style={{ opacity: isOpen ? 0 : 1 }}\n          >\n            <Check size={17} />\n          </span>\n        )}\n      </div>\n      <span role=\"status\" className=\"sr-only\">\n        {announcement}\n      </span>\n    </div>\n  );\n}\n\nexport default AccessRequest;\n",
      "content": "\"use client\";\n\nimport {\n  useEffect,\n  useId,\n  useReducer,\n  useRef,\n  useState,\n  type Ref,\n} from \"react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { Check, ChevronDown, ShieldCheck, X } from \"lucide-react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport { useMediaQuery } from \"@/lib/use-media-query\";\nimport { cn } from \"@/lib/utils\";\nimport { AccessRequestRow } from \"./access-request-row\";\nimport {\n  accessRequestReducer,\n  createAccessRequestState,\n  type AccessDecision,\n  type AccessRequester,\n  type AccessRequestEntry,\n} from \"./access-request-state\";\nimport { useAccessRequestMotion } from \"./use-access-request-motion\";\n\nexport type { AccessDecision, AccessRequester } from \"./access-request-state\";\n\ngsap.registerPlugin(useGSAP);\n\nexport interface AccessRequestProps {\n  requests: readonly AccessRequester[];\n  onDecision: (\n    person: AccessRequester,\n    decision: AccessDecision,\n    signal: AbortSignal,\n  ) => void | Promise<void>;\n  onResolved?: (person: AccessRequester, decision: AccessDecision) => void;\n  onDismiss?: () => void;\n  expanded?: boolean;\n  defaultExpanded?: boolean;\n  onExpandedChange?: (expanded: boolean) => void;\n  disabled?: boolean;\n  width?: number;\n  stiffness?: number;\n  damping?: number;\n  speed?: number;\n  errorMessage?: string;\n  className?: string;\n  ref?: Ref<HTMLDivElement>;\n}\n\nconst controlClass =\n  \"flex size-8 cursor-pointer items-center justify-center rounded-lg text-zinc-500 outline-none hover:bg-zinc-100 hover:text-zinc-900 focus-visible:ring-2 focus-visible:ring-zinc-500 disabled:pointer-events-none pointer-coarse:size-10 dark:text-zinc-400 dark:hover:bg-white/5 dark:hover:text-white dark:focus-visible:ring-zinc-400\";\nconst colors = [\"#ede9fe\", \"#ffe4e6\", \"#fef9c3\", \"#dcfce7\"];\n\nfunction RequestAvatar({\n  person,\n  index,\n  expanded,\n  count,\n}: {\n  person: AccessRequester;\n  index: number;\n  expanded: boolean;\n  count: number;\n}) {\n  const [initialIndex] = useState(index);\n  const colorIndex =\n    Array.from(person.id).reduce(\n      (total, letter) => total + letter.charCodeAt(0),\n      0,\n    ) % colors.length;\n  return (\n    <div\n      data-request-avatar={person.id}\n      className=\"absolute top-0 left-0\"\n      style={{\n        transform: `translate(${expanded ? 16 : 16 + initialIndex * 21}px, ${expanded ? 81 + initialIndex * 56 : 16}px)`,\n        opacity: expanded || initialIndex < 2 ? 1 : 0,\n        zIndex: count - index + 1,\n      }}\n    >\n      <Avatar className=\"size-8 border-2 border-white dark:border-[#171717]\">\n        {person.avatar && (\n          <AvatarImage src={person.avatar} alt=\"\" className=\"object-cover\" />\n        )}\n        <AvatarFallback\n          className={cn(\n            \"text-[11px] font-semibold text-zinc-800\",\n            person.emoji && \"text-[20px]\",\n          )}\n          style={{ backgroundColor: person.color ?? colors[colorIndex] }}\n        >\n          {person.emoji ??\n            person.name\n              .split(/\\s+/)\n              .filter(Boolean)\n              .slice(0, 2)\n              .map((word) => word[0])\n              .join(\"\")}\n        </AvatarFallback>\n      </Avatar>\n    </div>\n  );\n}\n\nexport function AccessRequest({\n  requests,\n  onDecision,\n  onResolved,\n  onDismiss,\n  expanded,\n  defaultExpanded = false,\n  onExpandedChange,\n  disabled = false,\n  width = 360,\n  stiffness = 420,\n  damping = 28,\n  speed = 1,\n  errorMessage = \"Couldn’t save this decision. Please try again.\",\n  className,\n  ref,\n}: AccessRequestProps) {\n  const [state, dispatch] = useReducer(\n    accessRequestReducer,\n    requests,\n    createAccessRequestState,\n  );\n  const [ownExpanded, setOwnExpanded] = useState(defaultExpanded);\n  const [dismissing, setDismissing] = useState(false);\n  const [dismissed, setDismissed] = useState(false);\n  const [announcement, setAnnouncement] = useState(\"\");\n  const [initialExpanded] = useState(expanded ?? defaultExpanded);\n  const [initialCount] = useState(state.entries.length);\n  const rootRef = useRef<HTMLDivElement>(null);\n  const panelRef = useRef<HTMLDivElement>(null);\n  const contentRef = useRef<HTMLDivElement>(null);\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const pendingRef = useRef(new Map<string, AbortController>());\n  const decisionFocusRef = useRef(new Set<string>());\n  const nextFocusRef = useRef<number | null>(null);\n  const callbacksRef = useRef({ onResolved, onDismiss });\n  const dismissingRef = useRef(false);\n  const uid = useId();\n  const isOpen = expanded ?? ownExpanded;\n  const reduced = useMediaQuery(\"(prefers-reduced-motion: reduce)\");\n  const count = state.entries.length;\n  const pendingCount = state.entries.filter(\n    (entry) => entry.phase === \"pending\",\n  ).length;\n  const reviewed = state.approved + state.declined;\n  const first = state.entries[0]?.person;\n  const summary = first\n    ? `${first.handle ? `@${first.handle.replace(/^@/, \"\")}` : first.name}${count > 1 ? ` and ${count - 1} ${count === 2 ? \"other\" : \"others\"}` : \"\"}`\n    : \"All caught up\";\n  const signature = JSON.stringify(\n    state.entries.map((entry) => [entry.person.id, entry.phase, entry.error]),\n  );\n  const size = Number.isFinite(width)\n    ? Math.min(480, Math.max(280, width))\n    : 360;\n\n  useAccessRequestMotion({\n    rootRef,\n    panelRef,\n    contentRef,\n    signature,\n    expanded: isOpen,\n    stiffness,\n    damping,\n    speed,\n    enabled: !dismissed,\n  });\n  const { contextSafe } = useGSAP({ scope: rootRef });\n\n  useGSAP(\n    () => {\n      callbacksRef.current = { onResolved, onDismiss };\n    },\n    { dependencies: [onResolved, onDismiss] },\n  );\n\n  useEffect(() => {\n    dispatch({ type: \"sync\", requests });\n  }, [requests]);\n\n  useEffect(() => {\n    for (const [id, controller] of pendingRef.current) {\n      const entry = state.entries.find((item) => item.person.id === id);\n      if (!entry || entry.phase === \"removing\") {\n        controller.abort();\n        pendingRef.current.delete(id);\n      }\n    }\n  }, [state.entries]);\n\n  useEffect(() => {\n    const pending = pendingRef.current;\n    return () => {\n      for (const controller of pending.values()) controller.abort();\n      pending.clear();\n    };\n  }, []);\n\n  useGSAP(\n    () => {\n      if (nextFocusRef.current === null) return;\n      const rows = Array.from(\n        contentRef.current?.querySelectorAll<HTMLElement>(\n          \"[data-request-row]\",\n        ) ?? [],\n      );\n      const row = rows[Math.min(nextFocusRef.current, rows.length - 1)];\n      (isOpen\n        ? (row?.querySelector<HTMLButtonElement>(\"button:not(:disabled)\") ??\n          triggerRef.current)\n        : triggerRef.current\n      )?.focus({ preventScroll: true });\n      nextFocusRef.current = null;\n    },\n    { scope: rootRef, dependencies: [signature, isOpen] },\n  );\n\n  const changeExpanded = (next: boolean) => {\n    if (dismissingRef.current) return;\n    if (expanded === undefined) setOwnExpanded(next);\n    onExpandedChange?.(next);\n    if (!next) triggerRef.current?.focus({ preventScroll: true });\n  };\n\n  const decide = async (\n    entry: AccessRequestEntry,\n    decision: AccessDecision,\n  ) => {\n    const { person } = entry;\n    if (\n      disabled ||\n      dismissingRef.current ||\n      entry.phase !== \"idle\" ||\n      pendingRef.current.has(person.id)\n    )\n      return;\n    const controller = new AbortController();\n    pendingRef.current.set(person.id, controller);\n    const row = Array.from(\n      contentRef.current?.querySelectorAll<HTMLElement>(\"[data-request-row]\") ??\n        [],\n    ).find((element) => element.dataset.requestRow === person.id);\n    if (row?.contains(document.activeElement))\n      decisionFocusRef.current.add(person.id);\n    dispatch({ type: \"begin\", id: person.id, decision });\n    setAnnouncement(\n      `${decision === \"approved\" ? \"Approving\" : \"Declining\"} ${person.name}.`,\n    );\n    try {\n      await onDecision(person, decision, controller.signal);\n      if (controller.signal.aborted) return;\n      dispatch({ type: \"resolve\", id: person.id, decision });\n      setAnnouncement(`${person.name} ${decision}.`);\n    } catch {\n      if (!controller.signal.aborted) {\n        dispatch({ type: \"reject\", id: person.id, message: errorMessage });\n        setAnnouncement(\n          `Decision for ${person.name} failed. Please try again.`,\n        );\n        if (\n          decisionFocusRef.current.has(person.id) &&\n          document.activeElement === document.body\n        )\n          nextFocusRef.current = state.entries.findIndex(\n            (item) => item.person.id === person.id,\n          );\n      }\n    } finally {\n      if (pendingRef.current.get(person.id) === controller)\n        pendingRef.current.delete(person.id);\n    }\n  };\n\n  const finish = (entry: AccessRequestEntry) => {\n    const rows = Array.from(\n      contentRef.current?.querySelectorAll<HTMLElement>(\"[data-request-row]\") ??\n        [],\n    );\n    const row = rows.find(\n      (element) => element.dataset.requestRow === entry.person.id,\n    );\n    if (\n      row?.contains(document.activeElement) ||\n      (decisionFocusRef.current.has(entry.person.id) &&\n        document.activeElement === document.body)\n    )\n      nextFocusRef.current = state.entries.findIndex(\n        (item) => item.person.id === entry.person.id,\n      );\n    decisionFocusRef.current.delete(entry.person.id);\n    dispatch({ type: \"remove\", id: entry.person.id });\n    if (entry.phase === \"decided\" && entry.decision)\n      callbacksRef.current.onResolved?.(entry.person, entry.decision);\n  };\n\n  const dismiss = () =>\n    contextSafe(() => {\n      if (dismissingRef.current) return;\n      dismissingRef.current = true;\n      setDismissing(true);\n      for (const [id, controller] of pendingRef.current) {\n        controller.abort();\n        dispatch({ type: \"reject\", id, message: \"Decision cancelled.\" });\n      }\n      pendingRef.current.clear();\n      gsap.to(rootRef.current, {\n        autoAlpha: 0,\n        y: reduced ? 0 : -8,\n        scale: reduced ? 1 : 0.97,\n        duration: reduced ? 0 : 0.18,\n        ease: \"power2.in\",\n        overwrite: \"auto\",\n        onComplete: () => {\n          setDismissed(true);\n          callbacksRef.current.onDismiss?.();\n        },\n      });\n    })();\n\n  if (dismissed) return null;\n\n  return (\n    <div\n      ref={(node) => {\n        rootRef.current = node;\n        if (typeof ref === \"function\") ref(node);\n        else if (ref) ref.current = node;\n      }}\n      data-slot=\"access-request\"\n      data-expanded={isOpen}\n      role=\"region\"\n      aria-labelledby={`${uid}-heading`}\n      className={cn(\n        \"relative max-w-full overflow-hidden rounded-[20px] border border-zinc-200 bg-white text-zinc-900 shadow-[0_8px_30px_-12px_#00000020] dark:border-white/[0.07] dark:bg-[#171717] dark:text-zinc-100 dark:shadow-[0_8px_30px_-12px_#00000050]\",\n        className,\n      )}\n      style={{ width: size }}\n      onKeyDown={(event) => {\n        if (event.key === \"Escape\") {\n          event.preventDefault();\n          event.stopPropagation();\n          if (isOpen) changeExpanded(false);\n          else dismiss();\n        }\n      }}\n    >\n      <div className=\"relative h-16\">\n        <button\n          type=\"button\"\n          data-request-compact\n          aria-expanded={isOpen}\n          aria-controls={`${uid}-panel`}\n          aria-label={\n            count\n              ? `Review ${count} access ${count === 1 ? \"request\" : \"requests\"}`\n              : \"Review completed requests\"\n          }\n          aria-hidden={isOpen}\n          inert={isOpen || dismissing}\n          tabIndex={isOpen ? -1 : 0}\n          onClick={() => changeExpanded(true)}\n          className=\"absolute inset-y-0 left-0 right-20 cursor-pointer rounded-l-[20px] text-left outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-zinc-400\"\n          style={{ opacity: initialExpanded ? 0 : 1 }}\n        >\n          <span\n            data-request-summary\n            className=\"absolute inset-y-0 left-0 flex min-w-0 flex-col justify-center\"\n            style={{\n              transform: `translateX(${initialCount > 2 ? 108 : initialCount > 1 ? 84 : initialCount ? 60 : 58}px)`,\n            }}\n          >\n            <span className=\"truncate text-[12px] leading-5 font-medium\">\n              {summary}\n            </span>\n            <span className=\"truncate text-[11px] leading-4 text-zinc-500 dark:text-zinc-400\">\n              {pendingCount\n                ? `Reviewing ${pendingCount} ${pendingCount === 1 ? \"request\" : \"requests\"}…`\n                : count\n                  ? \"requested access\"\n                  : `${reviewed} ${reviewed === 1 ? \"request\" : \"requests\"} reviewed`}\n            </span>\n          </span>\n        </button>\n        <h3\n          id={`${uid}-heading`}\n          data-request-heading\n          className=\"pointer-events-none absolute top-[22px] left-4 text-[13px] leading-5 font-medium\"\n          style={{ opacity: initialExpanded ? 1 : 0 }}\n        >\n          Access requests\n        </h3>\n        <div className=\"absolute top-4 right-11 pointer-coarse:top-3\">\n          <button\n            ref={triggerRef}\n            type=\"button\"\n            aria-label={\n              isOpen ? \"Collapse access requests\" : \"Expand access requests\"\n            }\n            aria-expanded={isOpen}\n            aria-controls={`${uid}-panel`}\n            disabled={dismissing}\n            onClick={() => changeExpanded(!isOpen)}\n            className={controlClass}\n          >\n            <ChevronDown\n              data-request-chevron\n              size={17}\n              aria-hidden=\"true\"\n              style={{\n                transform: initialExpanded ? \"rotate(180deg)\" : undefined,\n              }}\n            />\n          </button>\n        </div>\n        <div className=\"absolute top-4 right-2 pointer-coarse:top-3\">\n          <button\n            type=\"button\"\n            aria-label=\"Dismiss access requests\"\n            onClick={dismiss}\n            disabled={dismissing}\n            className={controlClass}\n          >\n            <X size={17} aria-hidden=\"true\" />\n          </button>\n        </div>\n      </div>\n      <div\n        ref={panelRef}\n        id={`${uid}-panel`}\n        inert={!isOpen || dismissing}\n        aria-hidden={!isOpen}\n        className=\"overflow-hidden\"\n        style={{ height: initialExpanded ? \"auto\" : 0 }}\n      >\n        <div ref={contentRef} style={{ opacity: initialExpanded ? 1 : 0 }}>\n          <div className=\"mx-4 h-px bg-zinc-100 dark:bg-white/[0.07]\" />\n          {count ? (\n            <ul className=\"relative py-2\">\n              {state.entries.map((entry) => (\n                <AccessRequestRow\n                  key={entry.person.id}\n                  entry={entry}\n                  expanded={isOpen}\n                  disabled={disabled || dismissing}\n                  reduced={reduced}\n                  onDecision={(decision) => void decide(entry, decision)}\n                  onExit={() => finish(entry)}\n                />\n              ))}\n            </ul>\n          ) : (\n            <div className=\"flex flex-col items-center px-5 py-8 text-center\">\n              <span className=\"mb-3 flex size-10 items-center justify-center rounded-full bg-emerald-50 text-emerald-700 dark:bg-emerald-400/10 dark:text-emerald-400\">\n                <ShieldCheck size={20} aria-hidden=\"true\" />\n              </span>\n              <p className=\"text-[13px] font-medium\">\n                {reviewed\n                  ? \"Everyone’s accounted for.\"\n                  : \"No pending requests.\"}\n              </p>\n              <p className=\"mt-1.5 text-[11px] leading-5 text-zinc-500 dark:text-zinc-400\">\n                {reviewed\n                  ? `${state.approved} approved · ${state.declined} declined`\n                  : \"New access requests will appear here.\"}\n              </p>\n            </div>\n          )}\n        </div>\n      </div>\n      <div aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0\">\n        {state.entries.map(({ person }, index) => (\n          <RequestAvatar\n            key={person.id}\n            person={person}\n            index={index}\n            expanded={initialExpanded}\n            count={state.entries.length}\n          />\n        ))}\n        <span\n          data-request-overflow\n          className=\"absolute top-0 left-0 flex size-8 items-center justify-center rounded-full bg-zinc-100 text-[12px] font-medium text-zinc-600 dark:bg-[#272727] dark:text-zinc-300\"\n          style={{\n            transform: \"translate(58px, 16px)\",\n            opacity: !initialExpanded && initialCount > 2 ? 1 : 0,\n          }}\n        >\n          {Math.max(0, count - 2)}\n        </span>\n        {!count && (\n          <span\n            className=\"absolute top-4 left-4 flex size-8 items-center justify-center rounded-full bg-emerald-50 text-emerald-700 dark:bg-emerald-400/10 dark:text-emerald-400\"\n            style={{ opacity: isOpen ? 0 : 1 }}\n          >\n            <Check size={17} />\n          </span>\n        )}\n      </div>\n      <span role=\"status\" className=\"sr-only\">\n        {announcement}\n      </span>\n    </div>\n  );\n}\n\nexport default AccessRequest;\n",
      "target": "@ui/access-request.tsx"
    },
    {
      "name": "access-request-row.tsx",
      "path": "registry/ui/access-request-row.tsx",
      "type": "registry:ui",
      "code": "\"use client\";\n\nimport { useRef } from \"react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { Check, LoaderCircle, X } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport type {\n  AccessDecision,\n  AccessRequestEntry,\n} from \"./access-request-state\";\n\ngsap.registerPlugin(useGSAP);\n\ninterface AccessRequestRowProps {\n  entry: AccessRequestEntry;\n  expanded: boolean;\n  disabled: boolean;\n  reduced: boolean;\n  onDecision: (decision: AccessDecision) => void;\n  onExit: () => void;\n}\n\nconst actionClass =\n  \"relative flex min-h-7 w-16 min-w-16 cursor-pointer items-center justify-center gap-1 rounded-full border px-2 text-[11px] leading-4 font-medium outline-none focus-visible:ring-2 focus-visible:ring-current focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-wait disabled:opacity-50 pointer-coarse:min-h-10 dark:focus-visible:ring-offset-[#171717]\";\n\nexport function AccessRequestRow({\n  entry,\n  expanded,\n  disabled,\n  reduced,\n  onDecision,\n  onExit,\n}: AccessRequestRowProps) {\n  const rowRef = useRef<HTMLLIElement>(null);\n  const callbacksRef = useRef(onExit);\n  const { person, phase, decision, error } = entry;\n  const leaving = phase === \"decided\" || phase === \"removing\";\n  const busy = phase === \"pending\";\n\n  useGSAP(\n    () => {\n      callbacksRef.current = onExit;\n    },\n    { dependencies: [onExit] },\n  );\n\n  useGSAP(\n    () => {\n      const face = rowRef.current?.querySelector(\"[data-request-face]\");\n      if (!face) return;\n      const duration = reduced ? 0 : 0.18;\n      if (leaving) {\n        const timeline = gsap.timeline({\n          onComplete: () => callbacksRef.current(),\n        });\n        timeline.to(\n          face,\n          {\n            x: reduced ? 0 : decision === \"approved\" ? 10 : -10,\n            autoAlpha: 0,\n            scale: reduced ? 1 : 0.98,\n            duration: expanded ? duration : 0,\n            ease: \"power2.in\",\n          },\n          expanded && phase === \"decided\" && !reduced ? 0.25 : 0,\n        );\n        return;\n      }\n      gsap.to(face, {\n        x: 0,\n        autoAlpha: 1,\n        scale: 1,\n        duration,\n        overwrite: \"auto\",\n      });\n      if (busy && expanded && !reduced)\n        gsap.to(\"[data-request-spinner]\", {\n          rotation: 360,\n          duration: 0.8,\n          repeat: -1,\n          ease: \"none\",\n        });\n    },\n    {\n      dependencies: [phase, decision, expanded, reduced],\n      scope: rowRef,\n      revertOnUpdate: true,\n    },\n  );\n\n  return (\n    <li\n      ref={rowRef}\n      data-request-row={person.id}\n      data-phase={phase}\n      data-decision={decision}\n      className=\"relative min-w-0 list-none\"\n      aria-busy={busy}\n    >\n      <div\n        data-request-face\n        className=\"grid min-h-14 grid-cols-[32px_minmax(0,1fr)_auto] items-center gap-x-2.5 px-4 py-2\"\n      >\n        <span data-request-anchor className=\"size-8\" aria-hidden=\"true\" />\n        <div className=\"min-w-0\">\n          <p\n            className=\"truncate text-[12px] leading-5 font-medium\"\n            title={person.name}\n          >\n            {person.name}\n          </p>\n          {person.description && (\n            <p\n              className=\"truncate text-[10px] leading-4 text-zinc-500 dark:text-zinc-400\"\n              title={person.description}\n            >\n              {person.description}\n            </p>\n          )}\n        </div>\n        {phase === \"decided\" ? (\n          <span\n            className={cn(\n              \"flex items-center gap-1.5 text-[11px] font-medium\",\n              decision === \"approved\"\n                ? \"text-emerald-700 dark:text-emerald-400\"\n                : \"text-zinc-500 dark:text-zinc-400\",\n            )}\n          >\n            {decision === \"approved\" ? (\n              <Check size={13} aria-hidden=\"true\" />\n            ) : (\n              <X size={13} aria-hidden=\"true\" />\n            )}\n            {decision === \"approved\" ? \"Approved\" : \"Declined\"}\n          </span>\n        ) : (\n          <div className=\"flex items-center gap-1.5\">\n            {([\"approved\", \"declined\"] as const).map((value) => (\n              <button\n                key={value}\n                type=\"button\"\n                disabled={busy || disabled || leaving}\n                aria-label={`${value === \"approved\" ? \"Approve\" : \"Decline\"} ${person.name}`}\n                onClick={() => onDecision(value)}\n                className={cn(\n                  actionClass,\n                  value === \"approved\"\n                    ? \"border-transparent bg-zinc-100 hover:bg-zinc-200 dark:bg-white/10 dark:hover:bg-white/15\"\n                    : \"border-zinc-200 text-zinc-500 hover:bg-zinc-100 hover:text-zinc-900 dark:border-white/10 dark:text-zinc-400 dark:hover:bg-white/5 dark:hover:text-zinc-100\",\n                )}\n              >\n                {busy && decision === value ? (\n                  <LoaderCircle\n                    data-request-spinner\n                    size={12}\n                    aria-hidden=\"true\"\n                  />\n                ) : value === \"approved\" ? (\n                  \"Approve\"\n                ) : (\n                  \"Decline\"\n                )}\n              </button>\n            ))}\n          </div>\n        )}\n        {error && (\n          <p\n            role=\"alert\"\n            className=\"col-start-2 col-end-4 pt-1.5 text-[11px] leading-4 text-red-600 dark:text-red-400\"\n          >\n            {error}\n          </p>\n        )}\n      </div>\n    </li>\n  );\n}\n",
      "content": "\"use client\";\n\nimport { useRef } from \"react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { Check, LoaderCircle, X } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport type {\n  AccessDecision,\n  AccessRequestEntry,\n} from \"./access-request-state\";\n\ngsap.registerPlugin(useGSAP);\n\ninterface AccessRequestRowProps {\n  entry: AccessRequestEntry;\n  expanded: boolean;\n  disabled: boolean;\n  reduced: boolean;\n  onDecision: (decision: AccessDecision) => void;\n  onExit: () => void;\n}\n\nconst actionClass =\n  \"relative flex min-h-7 w-16 min-w-16 cursor-pointer items-center justify-center gap-1 rounded-full border px-2 text-[11px] leading-4 font-medium outline-none focus-visible:ring-2 focus-visible:ring-current focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-wait disabled:opacity-50 pointer-coarse:min-h-10 dark:focus-visible:ring-offset-[#171717]\";\n\nexport function AccessRequestRow({\n  entry,\n  expanded,\n  disabled,\n  reduced,\n  onDecision,\n  onExit,\n}: AccessRequestRowProps) {\n  const rowRef = useRef<HTMLLIElement>(null);\n  const callbacksRef = useRef(onExit);\n  const { person, phase, decision, error } = entry;\n  const leaving = phase === \"decided\" || phase === \"removing\";\n  const busy = phase === \"pending\";\n\n  useGSAP(\n    () => {\n      callbacksRef.current = onExit;\n    },\n    { dependencies: [onExit] },\n  );\n\n  useGSAP(\n    () => {\n      const face = rowRef.current?.querySelector(\"[data-request-face]\");\n      if (!face) return;\n      const duration = reduced ? 0 : 0.18;\n      if (leaving) {\n        const timeline = gsap.timeline({\n          onComplete: () => callbacksRef.current(),\n        });\n        timeline.to(\n          face,\n          {\n            x: reduced ? 0 : decision === \"approved\" ? 10 : -10,\n            autoAlpha: 0,\n            scale: reduced ? 1 : 0.98,\n            duration: expanded ? duration : 0,\n            ease: \"power2.in\",\n          },\n          expanded && phase === \"decided\" && !reduced ? 0.25 : 0,\n        );\n        return;\n      }\n      gsap.to(face, {\n        x: 0,\n        autoAlpha: 1,\n        scale: 1,\n        duration,\n        overwrite: \"auto\",\n      });\n      if (busy && expanded && !reduced)\n        gsap.to(\"[data-request-spinner]\", {\n          rotation: 360,\n          duration: 0.8,\n          repeat: -1,\n          ease: \"none\",\n        });\n    },\n    {\n      dependencies: [phase, decision, expanded, reduced],\n      scope: rowRef,\n      revertOnUpdate: true,\n    },\n  );\n\n  return (\n    <li\n      ref={rowRef}\n      data-request-row={person.id}\n      data-phase={phase}\n      data-decision={decision}\n      className=\"relative min-w-0 list-none\"\n      aria-busy={busy}\n    >\n      <div\n        data-request-face\n        className=\"grid min-h-14 grid-cols-[32px_minmax(0,1fr)_auto] items-center gap-x-2.5 px-4 py-2\"\n      >\n        <span data-request-anchor className=\"size-8\" aria-hidden=\"true\" />\n        <div className=\"min-w-0\">\n          <p\n            className=\"truncate text-[12px] leading-5 font-medium\"\n            title={person.name}\n          >\n            {person.name}\n          </p>\n          {person.description && (\n            <p\n              className=\"truncate text-[10px] leading-4 text-zinc-500 dark:text-zinc-400\"\n              title={person.description}\n            >\n              {person.description}\n            </p>\n          )}\n        </div>\n        {phase === \"decided\" ? (\n          <span\n            className={cn(\n              \"flex items-center gap-1.5 text-[11px] font-medium\",\n              decision === \"approved\"\n                ? \"text-emerald-700 dark:text-emerald-400\"\n                : \"text-zinc-500 dark:text-zinc-400\",\n            )}\n          >\n            {decision === \"approved\" ? (\n              <Check size={13} aria-hidden=\"true\" />\n            ) : (\n              <X size={13} aria-hidden=\"true\" />\n            )}\n            {decision === \"approved\" ? \"Approved\" : \"Declined\"}\n          </span>\n        ) : (\n          <div className=\"flex items-center gap-1.5\">\n            {([\"approved\", \"declined\"] as const).map((value) => (\n              <button\n                key={value}\n                type=\"button\"\n                disabled={busy || disabled || leaving}\n                aria-label={`${value === \"approved\" ? \"Approve\" : \"Decline\"} ${person.name}`}\n                onClick={() => onDecision(value)}\n                className={cn(\n                  actionClass,\n                  value === \"approved\"\n                    ? \"border-transparent bg-zinc-100 hover:bg-zinc-200 dark:bg-white/10 dark:hover:bg-white/15\"\n                    : \"border-zinc-200 text-zinc-500 hover:bg-zinc-100 hover:text-zinc-900 dark:border-white/10 dark:text-zinc-400 dark:hover:bg-white/5 dark:hover:text-zinc-100\",\n                )}\n              >\n                {busy && decision === value ? (\n                  <LoaderCircle\n                    data-request-spinner\n                    size={12}\n                    aria-hidden=\"true\"\n                  />\n                ) : value === \"approved\" ? (\n                  \"Approve\"\n                ) : (\n                  \"Decline\"\n                )}\n              </button>\n            ))}\n          </div>\n        )}\n        {error && (\n          <p\n            role=\"alert\"\n            className=\"col-start-2 col-end-4 pt-1.5 text-[11px] leading-4 text-red-600 dark:text-red-400\"\n          >\n            {error}\n          </p>\n        )}\n      </div>\n    </li>\n  );\n}\n",
      "target": "@ui/access-request-row.tsx"
    },
    {
      "name": "access-request-state.ts",
      "path": "registry/ui/access-request-state.ts",
      "type": "registry:ui",
      "code": "export type AccessDecision = \"approved\" | \"declined\";\n\nexport interface AccessRequester {\n  id: string;\n  name: string;\n  handle?: string;\n  avatar?: string;\n  emoji?: string;\n  color?: string;\n  description?: string;\n}\n\nexport interface AccessRequestEntry {\n  person: AccessRequester;\n  phase: \"idle\" | \"pending\" | \"decided\" | \"removing\";\n  decision?: AccessDecision;\n  error?: string;\n}\n\nexport interface AccessRequestState {\n  entries: AccessRequestEntry[];\n  settledIds: string[];\n  approved: number;\n  declined: number;\n}\n\nexport type AccessRequestEvent =\n  | { type: \"sync\"; requests: readonly AccessRequester[] }\n  | { type: \"begin\" | \"resolve\"; id: string; decision: AccessDecision }\n  | { type: \"reject\"; id: string; message: string }\n  | { type: \"remove\"; id: string };\n\nexport function createAccessRequestState(\n  requests: readonly AccessRequester[],\n): AccessRequestState {\n  return {\n    entries: Array.from(\n      new Map(requests.map((person) => [person.id, person])).values(),\n    ).map((person) => ({ person, phase: \"idle\" })),\n    settledIds: [],\n    approved: 0,\n    declined: 0,\n  };\n}\n\nexport function accessRequestReducer(\n  state: AccessRequestState,\n  event: AccessRequestEvent,\n): AccessRequestState {\n  if (event.type === \"sync\") {\n    const incoming = new Map(\n      event.requests.map((person) => [person.id, person]),\n    );\n    const present = new Set(state.entries.map((entry) => entry.person.id));\n    const entries = state.entries.map((entry): AccessRequestEntry => {\n      const person = incoming.get(entry.person.id);\n      if (person)\n        return entry.phase === \"removing\"\n          ? { person, phase: \"idle\" }\n          : { ...entry, person };\n      return entry.phase === \"decided\"\n        ? entry\n        : { ...entry, phase: \"removing\", error: undefined };\n    });\n    for (const person of incoming.values()) {\n      if (!present.has(person.id) && !state.settledIds.includes(person.id))\n        entries.push({ person, phase: \"idle\" });\n    }\n    return {\n      ...state,\n      entries,\n      settledIds: state.settledIds.filter((id) => incoming.has(id)),\n    };\n  }\n\n  const entry = state.entries.find((item) => item.person.id === event.id);\n  if (!entry) return state;\n  let next: AccessRequestEntry;\n\n  switch (event.type) {\n    case \"begin\":\n      if (entry.phase !== \"idle\") return state;\n      next = {\n        ...entry,\n        phase: \"pending\",\n        decision: event.decision,\n        error: undefined,\n      };\n      break;\n    case \"resolve\":\n      if (entry.phase !== \"pending\" || entry.decision !== event.decision)\n        return state;\n      next = { ...entry, phase: \"decided\", error: undefined };\n      break;\n    case \"reject\":\n      if (entry.phase !== \"pending\") return state;\n      next = {\n        ...entry,\n        phase: \"idle\",\n        decision: undefined,\n        error: event.message,\n      };\n      break;\n    case \"remove\":\n      if (entry.phase !== \"decided\" && entry.phase !== \"removing\") return state;\n      return {\n        ...state,\n        entries: state.entries.filter((item) => item.person.id !== event.id),\n        settledIds:\n          entry.phase === \"decided\"\n            ? [...state.settledIds, event.id]\n            : state.settledIds,\n        approved:\n          state.approved +\n          Number(entry.phase === \"decided\" && entry.decision === \"approved\"),\n        declined:\n          state.declined +\n          Number(entry.phase === \"decided\" && entry.decision === \"declined\"),\n      };\n  }\n\n  return {\n    ...state,\n    entries: state.entries.map((item) =>\n      item.person.id === event.id ? next : item,\n    ),\n  };\n}\n",
      "content": "export type AccessDecision = \"approved\" | \"declined\";\n\nexport interface AccessRequester {\n  id: string;\n  name: string;\n  handle?: string;\n  avatar?: string;\n  emoji?: string;\n  color?: string;\n  description?: string;\n}\n\nexport interface AccessRequestEntry {\n  person: AccessRequester;\n  phase: \"idle\" | \"pending\" | \"decided\" | \"removing\";\n  decision?: AccessDecision;\n  error?: string;\n}\n\nexport interface AccessRequestState {\n  entries: AccessRequestEntry[];\n  settledIds: string[];\n  approved: number;\n  declined: number;\n}\n\nexport type AccessRequestEvent =\n  | { type: \"sync\"; requests: readonly AccessRequester[] }\n  | { type: \"begin\" | \"resolve\"; id: string; decision: AccessDecision }\n  | { type: \"reject\"; id: string; message: string }\n  | { type: \"remove\"; id: string };\n\nexport function createAccessRequestState(\n  requests: readonly AccessRequester[],\n): AccessRequestState {\n  return {\n    entries: Array.from(\n      new Map(requests.map((person) => [person.id, person])).values(),\n    ).map((person) => ({ person, phase: \"idle\" })),\n    settledIds: [],\n    approved: 0,\n    declined: 0,\n  };\n}\n\nexport function accessRequestReducer(\n  state: AccessRequestState,\n  event: AccessRequestEvent,\n): AccessRequestState {\n  if (event.type === \"sync\") {\n    const incoming = new Map(\n      event.requests.map((person) => [person.id, person]),\n    );\n    const present = new Set(state.entries.map((entry) => entry.person.id));\n    const entries = state.entries.map((entry): AccessRequestEntry => {\n      const person = incoming.get(entry.person.id);\n      if (person)\n        return entry.phase === \"removing\"\n          ? { person, phase: \"idle\" }\n          : { ...entry, person };\n      return entry.phase === \"decided\"\n        ? entry\n        : { ...entry, phase: \"removing\", error: undefined };\n    });\n    for (const person of incoming.values()) {\n      if (!present.has(person.id) && !state.settledIds.includes(person.id))\n        entries.push({ person, phase: \"idle\" });\n    }\n    return {\n      ...state,\n      entries,\n      settledIds: state.settledIds.filter((id) => incoming.has(id)),\n    };\n  }\n\n  const entry = state.entries.find((item) => item.person.id === event.id);\n  if (!entry) return state;\n  let next: AccessRequestEntry;\n\n  switch (event.type) {\n    case \"begin\":\n      if (entry.phase !== \"idle\") return state;\n      next = {\n        ...entry,\n        phase: \"pending\",\n        decision: event.decision,\n        error: undefined,\n      };\n      break;\n    case \"resolve\":\n      if (entry.phase !== \"pending\" || entry.decision !== event.decision)\n        return state;\n      next = { ...entry, phase: \"decided\", error: undefined };\n      break;\n    case \"reject\":\n      if (entry.phase !== \"pending\") return state;\n      next = {\n        ...entry,\n        phase: \"idle\",\n        decision: undefined,\n        error: event.message,\n      };\n      break;\n    case \"remove\":\n      if (entry.phase !== \"decided\" && entry.phase !== \"removing\") return state;\n      return {\n        ...state,\n        entries: state.entries.filter((item) => item.person.id !== event.id),\n        settledIds:\n          entry.phase === \"decided\"\n            ? [...state.settledIds, event.id]\n            : state.settledIds,\n        approved:\n          state.approved +\n          Number(entry.phase === \"decided\" && entry.decision === \"approved\"),\n        declined:\n          state.declined +\n          Number(entry.phase === \"decided\" && entry.decision === \"declined\"),\n      };\n  }\n\n  return {\n    ...state,\n    entries: state.entries.map((item) =>\n      item.person.id === event.id ? next : item,\n    ),\n  };\n}\n",
      "target": "@ui/access-request-state.ts"
    },
    {
      "name": "use-access-request-motion.ts",
      "path": "registry/ui/use-access-request-motion.ts",
      "type": "registry:ui",
      "code": "\"use client\";\n\nimport { useRef, type RefObject } from \"react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { calcGeneratorDuration, spring } from \"motion\";\n\ngsap.registerPlugin(useGSAP);\n\nconst bounded = (value: number, fallback: number, min: number, max: number) =>\n  Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;\n\nfunction anchorPosition(anchor: HTMLElement, root: HTMLElement) {\n  let x = 0;\n  let y = 0;\n  let node: HTMLElement | null = anchor;\n  while (node && node !== root) {\n    x += node.offsetLeft;\n    y += node.offsetTop;\n    node = node.offsetParent instanceof HTMLElement ? node.offsetParent : null;\n  }\n  return { x, y };\n}\n\nexport function useAccessRequestMotion({\n  rootRef,\n  panelRef,\n  contentRef,\n  signature,\n  expanded,\n  stiffness,\n  damping,\n  speed,\n  enabled,\n}: {\n  rootRef: RefObject<HTMLDivElement | null>;\n  panelRef: RefObject<HTMLDivElement | null>;\n  contentRef: RefObject<HTMLDivElement | null>;\n  signature: string;\n  expanded: boolean;\n  stiffness: number;\n  damping: number;\n  speed: number;\n  enabled: boolean;\n}) {\n  const syncRef = useRef<((immediate?: boolean) => void) | null>(null);\n\n  useGSAP(\n    () => {\n      const root = rootRef.current;\n      const panel = panelRef.current;\n      const content = contentRef.current;\n      if (!root || !panel || !content || !enabled) return;\n      const response = spring({\n        keyframes: [0, 1],\n        stiffness: bounded(stiffness, 420, 80, 600),\n        damping: bounded(damping, 28, 16, 60),\n        restSpeed: 0.001,\n        restDelta: 0.001,\n      });\n      const settle = calcGeneratorDuration(response);\n      const ease = (progress: number) =>\n        progress === 1 ? 1 : response.next(progress * settle).value;\n      const media = gsap.matchMedia();\n      media.add(\n        {\n          reduced: \"(prefers-reduced-motion: reduce)\",\n          standard: \"(prefers-reduced-motion: no-preference)\",\n        },\n        (context) => {\n          let timeline: gsap.core.Timeline | null = null;\n          let positions = new Map<string, number>();\n          let wasOpen = root.dataset.expanded === \"true\";\n          const sync = context.add(\"sync\", (immediate = false) => {\n            const open = root.dataset.expanded === \"true\";\n            const opening = open && !wasOpen;\n            wasOpen = open;\n            const reduced = Boolean(context.conditions?.reduced);\n            const duration =\n              immediate || reduced\n                ? 0\n                : settle / 1000 / bounded(speed, 1, 0.5, 2);\n            const rows = Array.from(\n              content.querySelectorAll<HTMLElement>(\"[data-request-row]\"),\n            );\n            const rowMap = new Map(\n              rows.map((row) => [row.dataset.requestRow, row]),\n            );\n            const measurements = rows.map((row) => ({\n              row,\n              id: row.dataset.requestRow ?? \"\",\n              top: row.offsetTop,\n              currentY: Number(gsap.getProperty(row, \"y\")) || 0,\n            }));\n            const avatars = Array.from(\n              root.querySelectorAll<HTMLElement>(\"[data-request-avatar]\"),\n            );\n            const targets = avatars.map((avatar, index) => {\n              const row = rowMap.get(avatar.dataset.requestAvatar);\n              const anchor = row?.querySelector<HTMLElement>(\n                \"[data-request-anchor]\",\n              );\n              const position = anchor ? anchorPosition(anchor, root) : null;\n              const leaving =\n                row?.dataset.phase === \"decided\" ||\n                row?.dataset.phase === \"removing\";\n              return {\n                avatar,\n                x: open && position ? position.x : 16 + index * 21,\n                y: open && position ? position.y : 16,\n                visible: !leaving && (open || index < 2),\n                departing: open && leaving,\n                acknowledgement: row?.dataset.phase === \"decided\",\n                direction: row?.dataset.decision === \"approved\" ? 10 : -10,\n              };\n            });\n            const summaryX =\n              avatars.length > 2\n                ? 108\n                : avatars.length > 1\n                  ? 84\n                  : avatars.length\n                    ? 60\n                    : 58;\n            timeline?.kill();\n            timeline = gsap.timeline({\n              defaults: { duration, ease, overwrite: \"auto\" },\n            });\n            timeline.to(panel, { height: open ? content.offsetHeight : 0 }, 0);\n            timeline.to(\n              \"[data-request-compact]\",\n              {\n                autoAlpha: open ? 0 : 1,\n                duration: Math.min(duration, 0.14),\n                ease: \"power2.out\",\n              },\n              0,\n            );\n            timeline.to(\n              \"[data-request-heading]\",\n              {\n                autoAlpha: open ? 1 : 0,\n                x: open ? 0 : 6,\n                duration: Math.min(duration, 0.18),\n                ease: \"power2.out\",\n              },\n              open && duration ? 0.06 : 0,\n            );\n            timeline.to(\n              content,\n              {\n                opacity: open ? 1 : 0,\n                duration: Math.min(duration, 0.2),\n                ease: \"power2.out\",\n              },\n              open && duration ? 0.04 : 0,\n            );\n            timeline.to(\n              \"[data-request-chevron]\",\n              { rotation: open ? 180 : 0 },\n              0,\n            );\n            timeline.to(\n              \"[data-request-overflow]\",\n              {\n                x: 58,\n                y: 16,\n                autoAlpha: !open && avatars.length > 2 ? 1 : 0,\n                scale: open ? 0.8 : 1,\n                duration: Math.min(duration, 0.2),\n              },\n              0,\n            );\n            timeline.to(\"[data-request-summary]\", { x: summaryX }, 0);\n            gsap.set(\"[data-request-summary]\", {\n              width: Math.max(0, root.clientWidth - summaryX - 80),\n            });\n            for (const [\n              index,\n              { avatar, x, y, visible, departing, acknowledgement, direction },\n            ] of targets.entries()) {\n              timeline.to(\n                avatar,\n                {\n                  x: departing && duration ? x + direction : x,\n                  y,\n                  autoAlpha: visible ? 1 : 0,\n                  scale: visible ? 1 : departing ? 0.98 : 0.65,\n                  duration: visible ? duration : Math.min(duration, 0.18),\n                  ease: departing ? \"power2.in\" : ease,\n                },\n                departing && acknowledgement && duration\n                  ? 0.25\n                  : opening && duration\n                    ? index * 0.012\n                    : 0,\n              );\n            }\n            for (const { row, id, top, currentY } of measurements) {\n              const previous = positions.get(id);\n              if (previous !== undefined)\n                timeline.fromTo(\n                  row,\n                  { y: previous - top + currentY },\n                  { y: 0, immediateRender: true },\n                  0,\n                );\n            }\n            positions = new Map(measurements.map(({ id, top }) => [id, top]));\n          });\n          syncRef.current = (immediate) => sync(immediate);\n          sync(true);\n          let previousWidth = root.clientWidth;\n          let previousHeight = content.offsetHeight;\n          const observer = new ResizeObserver(() => {\n            const width = root.clientWidth;\n            const height = content.offsetHeight;\n            if (width === previousWidth && height === previousHeight) return;\n            previousWidth = width;\n            previousHeight = height;\n            sync();\n          });\n          observer.observe(content);\n          observer.observe(root);\n          return () => {\n            observer.disconnect();\n            timeline?.kill();\n            syncRef.current = null;\n          };\n        },\n        root,\n      );\n      return () => media.revert();\n    },\n    {\n      scope: rootRef,\n      dependencies: [stiffness, damping, speed, enabled],\n      revertOnUpdate: true,\n    },\n  );\n\n  useGSAP(\n    (context) => {\n      context.ignore(() => syncRef.current?.());\n    },\n    { scope: rootRef, dependencies: [signature, expanded] },\n  );\n}\n",
      "content": "\"use client\";\n\nimport { useRef, type RefObject } from \"react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { calcGeneratorDuration, spring } from \"motion\";\n\ngsap.registerPlugin(useGSAP);\n\nconst bounded = (value: number, fallback: number, min: number, max: number) =>\n  Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;\n\nfunction anchorPosition(anchor: HTMLElement, root: HTMLElement) {\n  let x = 0;\n  let y = 0;\n  let node: HTMLElement | null = anchor;\n  while (node && node !== root) {\n    x += node.offsetLeft;\n    y += node.offsetTop;\n    node = node.offsetParent instanceof HTMLElement ? node.offsetParent : null;\n  }\n  return { x, y };\n}\n\nexport function useAccessRequestMotion({\n  rootRef,\n  panelRef,\n  contentRef,\n  signature,\n  expanded,\n  stiffness,\n  damping,\n  speed,\n  enabled,\n}: {\n  rootRef: RefObject<HTMLDivElement | null>;\n  panelRef: RefObject<HTMLDivElement | null>;\n  contentRef: RefObject<HTMLDivElement | null>;\n  signature: string;\n  expanded: boolean;\n  stiffness: number;\n  damping: number;\n  speed: number;\n  enabled: boolean;\n}) {\n  const syncRef = useRef<((immediate?: boolean) => void) | null>(null);\n\n  useGSAP(\n    () => {\n      const root = rootRef.current;\n      const panel = panelRef.current;\n      const content = contentRef.current;\n      if (!root || !panel || !content || !enabled) return;\n      const response = spring({\n        keyframes: [0, 1],\n        stiffness: bounded(stiffness, 420, 80, 600),\n        damping: bounded(damping, 28, 16, 60),\n        restSpeed: 0.001,\n        restDelta: 0.001,\n      });\n      const settle = calcGeneratorDuration(response);\n      const ease = (progress: number) =>\n        progress === 1 ? 1 : response.next(progress * settle).value;\n      const media = gsap.matchMedia();\n      media.add(\n        {\n          reduced: \"(prefers-reduced-motion: reduce)\",\n          standard: \"(prefers-reduced-motion: no-preference)\",\n        },\n        (context) => {\n          let timeline: gsap.core.Timeline | null = null;\n          let positions = new Map<string, number>();\n          let wasOpen = root.dataset.expanded === \"true\";\n          const sync = context.add(\"sync\", (immediate = false) => {\n            const open = root.dataset.expanded === \"true\";\n            const opening = open && !wasOpen;\n            wasOpen = open;\n            const reduced = Boolean(context.conditions?.reduced);\n            const duration =\n              immediate || reduced\n                ? 0\n                : settle / 1000 / bounded(speed, 1, 0.5, 2);\n            const rows = Array.from(\n              content.querySelectorAll<HTMLElement>(\"[data-request-row]\"),\n            );\n            const rowMap = new Map(\n              rows.map((row) => [row.dataset.requestRow, row]),\n            );\n            const measurements = rows.map((row) => ({\n              row,\n              id: row.dataset.requestRow ?? \"\",\n              top: row.offsetTop,\n              currentY: Number(gsap.getProperty(row, \"y\")) || 0,\n            }));\n            const avatars = Array.from(\n              root.querySelectorAll<HTMLElement>(\"[data-request-avatar]\"),\n            );\n            const targets = avatars.map((avatar, index) => {\n              const row = rowMap.get(avatar.dataset.requestAvatar);\n              const anchor = row?.querySelector<HTMLElement>(\n                \"[data-request-anchor]\",\n              );\n              const position = anchor ? anchorPosition(anchor, root) : null;\n              const leaving =\n                row?.dataset.phase === \"decided\" ||\n                row?.dataset.phase === \"removing\";\n              return {\n                avatar,\n                x: open && position ? position.x : 16 + index * 21,\n                y: open && position ? position.y : 16,\n                visible: !leaving && (open || index < 2),\n                departing: open && leaving,\n                acknowledgement: row?.dataset.phase === \"decided\",\n                direction: row?.dataset.decision === \"approved\" ? 10 : -10,\n              };\n            });\n            const summaryX =\n              avatars.length > 2\n                ? 108\n                : avatars.length > 1\n                  ? 84\n                  : avatars.length\n                    ? 60\n                    : 58;\n            timeline?.kill();\n            timeline = gsap.timeline({\n              defaults: { duration, ease, overwrite: \"auto\" },\n            });\n            timeline.to(panel, { height: open ? content.offsetHeight : 0 }, 0);\n            timeline.to(\n              \"[data-request-compact]\",\n              {\n                autoAlpha: open ? 0 : 1,\n                duration: Math.min(duration, 0.14),\n                ease: \"power2.out\",\n              },\n              0,\n            );\n            timeline.to(\n              \"[data-request-heading]\",\n              {\n                autoAlpha: open ? 1 : 0,\n                x: open ? 0 : 6,\n                duration: Math.min(duration, 0.18),\n                ease: \"power2.out\",\n              },\n              open && duration ? 0.06 : 0,\n            );\n            timeline.to(\n              content,\n              {\n                opacity: open ? 1 : 0,\n                duration: Math.min(duration, 0.2),\n                ease: \"power2.out\",\n              },\n              open && duration ? 0.04 : 0,\n            );\n            timeline.to(\n              \"[data-request-chevron]\",\n              { rotation: open ? 180 : 0 },\n              0,\n            );\n            timeline.to(\n              \"[data-request-overflow]\",\n              {\n                x: 58,\n                y: 16,\n                autoAlpha: !open && avatars.length > 2 ? 1 : 0,\n                scale: open ? 0.8 : 1,\n                duration: Math.min(duration, 0.2),\n              },\n              0,\n            );\n            timeline.to(\"[data-request-summary]\", { x: summaryX }, 0);\n            gsap.set(\"[data-request-summary]\", {\n              width: Math.max(0, root.clientWidth - summaryX - 80),\n            });\n            for (const [\n              index,\n              { avatar, x, y, visible, departing, acknowledgement, direction },\n            ] of targets.entries()) {\n              timeline.to(\n                avatar,\n                {\n                  x: departing && duration ? x + direction : x,\n                  y,\n                  autoAlpha: visible ? 1 : 0,\n                  scale: visible ? 1 : departing ? 0.98 : 0.65,\n                  duration: visible ? duration : Math.min(duration, 0.18),\n                  ease: departing ? \"power2.in\" : ease,\n                },\n                departing && acknowledgement && duration\n                  ? 0.25\n                  : opening && duration\n                    ? index * 0.012\n                    : 0,\n              );\n            }\n            for (const { row, id, top, currentY } of measurements) {\n              const previous = positions.get(id);\n              if (previous !== undefined)\n                timeline.fromTo(\n                  row,\n                  { y: previous - top + currentY },\n                  { y: 0, immediateRender: true },\n                  0,\n                );\n            }\n            positions = new Map(measurements.map(({ id, top }) => [id, top]));\n          });\n          syncRef.current = (immediate) => sync(immediate);\n          sync(true);\n          let previousWidth = root.clientWidth;\n          let previousHeight = content.offsetHeight;\n          const observer = new ResizeObserver(() => {\n            const width = root.clientWidth;\n            const height = content.offsetHeight;\n            if (width === previousWidth && height === previousHeight) return;\n            previousWidth = width;\n            previousHeight = height;\n            sync();\n          });\n          observer.observe(content);\n          observer.observe(root);\n          return () => {\n            observer.disconnect();\n            timeline?.kill();\n            syncRef.current = null;\n          };\n        },\n        root,\n      );\n      return () => media.revert();\n    },\n    {\n      scope: rootRef,\n      dependencies: [stiffness, damping, speed, enabled],\n      revertOnUpdate: true,\n    },\n  );\n\n  useGSAP(\n    (context) => {\n      context.ignore(() => syncRef.current?.());\n    },\n    { scope: rootRef, dependencies: [signature, expanded] },\n  );\n}\n",
      "target": "@ui/use-access-request-motion.ts"
    },
    {
      "name": "avatar.tsx",
      "path": "components/ui/avatar.tsx",
      "type": "registry:ui",
      "code": "\"use client\";\n\nimport * as React from \"react\";\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Avatar = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full\",\n      className,\n    )}\n    {...props}\n  />\n));\nAvatar.displayName = AvatarPrimitive.Root.displayName;\n\nconst AvatarImage = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Image>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Image\n    ref={ref}\n    className={cn(\"aspect-square h-full w-full\", className)}\n    {...props}\n  />\n));\nAvatarImage.displayName = AvatarPrimitive.Image.displayName;\n\nconst AvatarFallback = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Fallback>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Fallback\n    ref={ref}\n    className={cn(\n      \"flex h-full w-full items-center justify-center rounded-full bg-muted\",\n      className,\n    )}\n    {...props}\n  />\n));\nAvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;\n\nexport { Avatar, AvatarImage, AvatarFallback };\n\nexport default Avatar;\n",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Avatar = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full\",\n      className,\n    )}\n    {...props}\n  />\n));\nAvatar.displayName = AvatarPrimitive.Root.displayName;\n\nconst AvatarImage = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Image>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Image\n    ref={ref}\n    className={cn(\"aspect-square h-full w-full\", className)}\n    {...props}\n  />\n));\nAvatarImage.displayName = AvatarPrimitive.Image.displayName;\n\nconst AvatarFallback = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Fallback>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Fallback\n    ref={ref}\n    className={cn(\n      \"flex h-full w-full items-center justify-center rounded-full bg-muted\",\n      className,\n    )}\n    {...props}\n  />\n));\nAvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;\n\nexport { Avatar, AvatarImage, AvatarFallback };\n\nexport default Avatar;\n",
      "target": "@ui/avatar.tsx"
    },
    {
      "name": "use-media-query.ts",
      "path": "lib/use-media-query.ts",
      "type": "registry:lib",
      "code": "\"use client\";\n\nimport { useCallback, useSyncExternalStore } from \"react\";\n\nexport function useMediaQuery(query: string) {\n  const subscribe = useCallback(\n    (onChange: () => void) => {\n      const media = window.matchMedia(query);\n      media.addEventListener(\"change\", onChange);\n      return () => media.removeEventListener(\"change\", onChange);\n    },\n    [query],\n  );\n  const getSnapshot = useCallback(() => window.matchMedia(query).matches, [query]);\n  return useSyncExternalStore(subscribe, getSnapshot, () => false);\n}\n",
      "content": "\"use client\";\n\nimport { useCallback, useSyncExternalStore } from \"react\";\n\nexport function useMediaQuery(query: string) {\n  const subscribe = useCallback(\n    (onChange: () => void) => {\n      const media = window.matchMedia(query);\n      media.addEventListener(\"change\", onChange);\n      return () => media.removeEventListener(\"change\", onChange);\n    },\n    [query],\n  );\n  const getSnapshot = useCallback(() => window.matchMedia(query).matches, [query]);\n  return useSyncExternalStore(subscribe, getSnapshot, () => false);\n}\n"
    }
  ]
}
