{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"agent-inbox","type":"registry:block","title":"Agent Inbox","description":"Human-in-the-loop approval queue: risk-tiered request items with expiry and detail expand, approve/deny flows that settle in place, and an action receipt card with scope, diff stats and undo.","author":"UI Lab","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"cssVars":{"light":{"wb-hover":"rgb(0 0 0 / 0.05)","wb-hover-subtle":"rgb(0 0 0 / 0.04)","wb-hover-strong":"rgb(0 0 0 / 0.1)","wb-hover-stronger":"rgb(0 0 0 / 0.1)","wb-inset":"rgb(0 0 0 / 0.05)","wb-inset-strong":"rgb(0 0 0 / 0.06)","wb-inset-subtle":"rgb(0 0 0 / 0.03)","wb-inset-faint":"rgb(0 0 0 / 0.02)","wb-code-inline":"rgb(0 0 0 / 0.07)","wb-code-block":"rgb(0 0 0 / 0.04)","wb-border":"rgb(0 0 0 / 0.12)","wb-border-subtle":"rgb(0 0 0 / 0.1)","wb-border-strong":"rgb(0 0 0 / 0.17)","wb-border-emphasis":"rgb(0 0 0 / 0.22)","wb-divider":"rgb(0 0 0 / 0.07)","wb-hairline":"rgb(0 0 0 / 0.12)","wb-hairline-soft":"rgb(0 0 0 / 0.1)","wb-hairline-subtle":"rgb(0 0 0 / 0.08)","wb-control-hairline":"rgb(0 0 0 / 0.08)","wb-control-tick":"rgb(0 0 0 / 0.2)","wb-resize-handle":"rgb(0 0 0 / 0.2)","wb-surface":"rgb(255 255 255)","wb-surface-translucent":"oklch(94.5% 0.002 250 / 0.75)","wb-overlay-scrim":"rgb(0 0 0 / 0.06)","wb-surface-raised":"rgb(255 255 255 / 0.95)","wb-surface-composer":"rgb(255 255 255 / 0.9)","wb-card":"rgb(0 0 0 / 0.04)","wb-inverse":"oklch(20.5% 0 0)","wb-inverse-fg":"rgb(255 255 255)","wb-accent-fg":"rgb(255 255 255)","wb-solid-control-fg":"oklch(20.5% 0 0)","wb-shimmer-dim":"rgb(0 0 0 / 0.4)","wb-shimmer-bright":"rgb(0 0 0 / 0.85)","wb-accent":"#339cff","wb-success":"oklch(59.6% 0.145 163.225)","wb-success-border":"oklch(69.6% 0.17 162.48)","wb-danger":"oklch(63.7% 0.237 25.331)","wb-danger-strong":"#fa423e","wb-danger-surface":"oklch(63.7% 0.237 25.331)","wb-warning":"#e25507","wb-warning-text":"oklch(64.6% 0.222 41.116)","wb-warning-hover":"oklch(70.5% 0.213 47.604)","wb-warning-surface":"#e25507"},"dark":{"wb-hover":"rgb(255 255 255 / 0.1)","wb-hover-subtle":"rgb(255 255 255 / 0.05)","wb-hover-strong":"rgb(255 255 255 / 0.1)","wb-hover-stronger":"rgb(255 255 255 / 0.15)","wb-inset":"rgb(255 255 255 / 0.05)","wb-inset-strong":"rgb(255 255 255 / 0.1)","wb-inset-subtle":"rgb(255 255 255 / 0.05)","wb-inset-faint":"rgb(255 255 255 / 0.03)","wb-code-inline":"rgb(255 255 255 / 0.1)","wb-code-block":"rgb(255 255 255 / 0.05)","wb-border":"rgb(255 255 255 / 0.1)","wb-border-subtle":"rgb(255 255 255 / 0.1)","wb-border-strong":"rgb(255 255 255 / 0.15)","wb-border-emphasis":"rgb(255 255 255 / 0.2)","wb-divider":"rgb(255 255 255 / 0.06)","wb-hairline":"rgb(255 255 255 / 0.157)","wb-hairline-soft":"rgb(255 255 255 / 0.15)","wb-hairline-subtle":"rgb(255 255 255 / 0.1)","wb-control-hairline":"rgb(255 255 255 / 0.08)","wb-control-tick":"rgb(255 255 255 / 0.3)","wb-resize-handle":"rgb(255 255 255 / 0.25)","wb-surface":"oklch(20.5% 0 0)","wb-surface-translucent":"oklch(20.5% 0 0 / 0.6)","wb-overlay-scrim":"rgb(0 0 0 / 0.24)","wb-surface-raised":"oklch(26.9% 0 0 / 0.95)","wb-surface-composer":"oklch(26.9% 0 0 / 0.9)","wb-card":"oklch(26.9% 0 0 / 0.5)","wb-inverse":"rgb(255 255 255)","wb-inverse-fg":"oklch(20.5% 0 0)","wb-accent-fg":"rgb(255 255 255)","wb-solid-control-fg":"oklch(20.5% 0 0)","wb-shimmer-dim":"rgb(255 255 255 / 0.4)","wb-shimmer-bright":"rgb(255 255 255 / 0.95)","wb-accent":"#339cff","wb-success":"#40c977","wb-success-border":"oklch(69.6% 0.17 162.48)","wb-danger":"#fa423e","wb-danger-strong":"#fa423e","wb-danger-surface":"oklch(63.7% 0.237 25.331)","wb-warning":"#ff8549","wb-warning-text":"#ff8549","wb-warning-hover":"#e25507","wb-warning-surface":"#e25507"}},"files":[{"path":"components/motion/agent-inbox/index.tsx","type":"registry:component","target":"@components/motion/agent-inbox/index.tsx","content":"\"use client\";\n// ui-lab-ten.vercel.app/components/blocks/agent-inbox\n\nimport { Check, ChevronRight } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  createContext,\n  type ReactNode,\n  useContext,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { EASE_OUT, SPRING_LAYOUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport type InboxRisk = \"low\" | \"medium\" | \"high\";\nexport type InboxStatus = \"pending\" | \"approved\" | \"denied\" | \"expired\";\nexport type AgentInboxVariant = \"card\" | \"quiet\";\n\nconst AgentInboxVariantContext = createContext<AgentInboxVariant>(\"card\");\n\ninterface InboxActionButtonProps {\n  variant?: \"ghost\" | \"primary\";\n  onClick?: () => void;\n  children?: ReactNode;\n}\n\n/** Self-contained ghost/primary button for `InboxItem`'s approve/deny row — deliberately not imported from `agent-thread` so this block distributes independently. */\nfunction InboxActionButton({ variant = \"ghost\", onClick, children }: InboxActionButtonProps) {\n  return (\n    <button\n      type=\"button\"\n      onClick={onClick}\n      className={cn(\n        \"flex h-7 items-center rounded-lg text-sm transition-colors\",\n        variant === \"ghost\" &&\n          \"px-2 text-muted-foreground hover:bg-[var(--wb-hover)] hover:text-foreground\",\n        variant === \"primary\" && \"bg-foreground px-3 text-background transition-opacity hover:opacity-85\",\n      )}\n    >\n      {children}\n    </button>\n  );\n}\n\ninterface InboxCountBadgeProps {\n  count: number;\n}\n\n/** Small pill in `AgentInbox`'s header — the digits crossfade with a short y-shift on change, matching `ThreadBranchSwitcher`'s readout. */\nfunction InboxCountBadge({ count }: InboxCountBadgeProps) {\n  const reduce = useReducedMotion() ?? false;\n\n  return (\n    <span className=\"relative inline-flex h-5 min-w-5 items-center justify-center overflow-hidden rounded-full bg-[var(--wb-accent)]/15 px-1.5 text-xs font-medium text-[var(--wb-accent)] tabular-nums\">\n      <AnimatePresence mode=\"popLayout\" initial={false}>\n        <motion.span\n          key={count}\n          initial={reduce ? { opacity: 0 } : { opacity: 0, y: 4 }}\n          animate={{ opacity: 1, y: 0 }}\n          exit={reduce ? { opacity: 0 } : { opacity: 0, y: -4 }}\n          transition={{ duration: 0.15, ease: EASE_OUT }}\n          className=\"absolute inset-0 flex items-center justify-center\"\n        >\n          {count}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n\nexport interface AgentInboxProps {\n  /** Card keeps the approval-queue shell; quiet becomes a flat semantic list. */\n  variant?: AgentInboxVariant;\n  title?: ReactNode;\n  /** Pending-count badge shown next to the title; omit to hide it entirely. */\n  count?: number;\n  /** Trailing slot for the caller's own controls, e.g. a \"Clear\" button. */\n  action?: ReactNode;\n  className?: string;\n  /** `InboxItem`s (or any other row, e.g. a standalone `ActionReceipt`) — hairline-separated top to bottom. */\n  children?: ReactNode;\n}\n\n/**\n * Approval-queue container — a hairline-ringed card (the same CSS-variable\n * box-shadow technique as `ThreadCard`) with a header row (title, optional\n * count badge, trailing action) above a stack of rows. Rows are rendered\n * directly as `children` and separated by a hairline via a descendant\n * selector, so any row shape (an `InboxItem`, a bare `ActionReceipt`) drops in\n * cleanly without the container needing to know its type.\n */\nexport function AgentInbox({\n  variant = \"card\",\n  title,\n  count,\n  action,\n  className,\n  children,\n}: AgentInboxProps) {\n  const quiet = variant === \"quiet\";\n\n  return (\n    <AgentInboxVariantContext.Provider value={variant}>\n      <div\n        data-slot=\"agent-inbox\"\n        data-variant={variant}\n        style={quiet ? undefined : { boxShadow: \"0 0 0 0.5px var(--wb-hairline)\" }}\n        className={cn(\n          \"flex flex-col overflow-hidden\",\n          quiet &&\n            \"border-[var(--wb-border-subtle)] border-y-[0.5px] bg-transparent\",\n          !quiet && \"rounded-2xl bg-[var(--wb-surface)]\",\n          className,\n        )}\n      >\n        {!quiet || title || count !== undefined || action ? (\n          <div\n            className={cn(\n              \"flex shrink-0 items-center gap-2 border-[var(--wb-divider)] border-b-[0.5px]\",\n              quiet ? \"h-9 px-0\" : \"h-11 px-4\",\n            )}\n          >\n            {title ? <span className=\"text-sm font-medium\">{title}</span> : null}\n            {count !== undefined ? <InboxCountBadge count={count} /> : null}\n            {action ? <div className=\"ml-auto shrink-0\">{action}</div> : null}\n          </div>\n        ) : null}\n        <div\n          data-slot=\"agent-inbox-items\"\n          className=\"flex flex-col [&>*+*]:border-[var(--wb-divider)] [&>*+*]:border-t-[0.5px]\"\n        >\n          {children}\n        </div>\n      </div>\n    </AgentInboxVariantContext.Provider>\n  );\n}\n\nexport interface InboxRiskBadgeProps {\n  risk: InboxRisk;\n  className?: string;\n}\n\n/** Uppercase risk pill — low is neutral, medium is amber, high is red, matching the library's warning/danger hues. */\nexport function InboxRiskBadge({ risk, className }: InboxRiskBadgeProps) {\n  return (\n    <span\n      className={cn(\n        \"inline-flex h-4.5 items-center rounded-full px-1.5 text-[10px] font-medium uppercase tracking-wide\",\n        risk === \"low\" && \"bg-[var(--wb-inset-strong)] text-muted-foreground\",\n        risk === \"medium\" && \"bg-[var(--wb-warning-surface)]/10 text-[var(--wb-warning)]\",\n        risk === \"high\" && \"bg-[var(--wb-danger-surface)]/10 text-[var(--wb-danger)]\",\n        className,\n      )}\n    >\n      {risk}\n    </span>\n  );\n}\n\nexport interface InboxItemProps {\n  /** Matches the parent AgentInbox variant; quiet renders a compact flat row. */\n  variant?: AgentInboxVariant;\n  /** 16px leading icon, e.g. `<Mail className=\"h-4 w-4\" />`. */\n  icon?: ReactNode;\n  /** Requesting agent, e.g. \"deploy-agent\". */\n  source?: ReactNode;\n  title: ReactNode;\n  description?: ReactNode;\n  risk?: InboxRisk;\n  /** Trailing countdown readout, e.g. \"expires in 2h\". */\n  expires?: ReactNode;\n  status?: InboxStatus;\n  onApprove?: () => void;\n  onDeny?: () => void;\n  approveLabel?: ReactNode;\n  denyLabel?: ReactNode;\n  /** Replaces the approve/deny row once `status` is no longer \"pending\". */\n  resolution?: ReactNode;\n  className?: string;\n  /** Optional detail region (e.g. a command preview or an `ActionReceipt`) revealed by a \"Details\" toggle. */\n  children?: ReactNode;\n}\n\n/**\n * One queued approval request. While `pending`, a ghost Deny and a primary\n * Approve button sit at the trailing edge; once resolved, `resolution`\n * replaces them and the rest of the row's content dims to `opacity-70` (the\n * resolution line stays at full opacity). The optional `children` region is\n * measured with a `ResizeObserver` and tweened open/closed (`EASE_OUT`,\n * 0.25s), shown instantly under `useReducedMotion()` — the same idiom as\n * `ThreadCollapse`. The root is a `motion.div` with `layout` so the height\n * change on resolution settles smoothly instead of snapping.\n */\nexport function InboxItem({\n  variant: variantProp,\n  icon,\n  source,\n  title,\n  description,\n  risk,\n  expires,\n  status = \"pending\",\n  onApprove,\n  onDeny,\n  approveLabel = \"Approve\",\n  denyLabel = \"Deny\",\n  resolution,\n  className,\n  children,\n}: InboxItemProps) {\n  const reduce = useReducedMotion() ?? false;\n  const inheritedVariant = useContext(AgentInboxVariantContext);\n  const variant = variantProp ?? inheritedVariant;\n  const [detailsOpen, setDetailsOpen] = useState(false);\n  const contentRef = useRef<HTMLDivElement>(null);\n  const [contentHeight, setContentHeight] = useState(0);\n\n  const pending = status === \"pending\";\n  const hasDetails = children !== undefined && children !== null;\n\n  useLayoutEffect(() => {\n    const node = contentRef.current;\n    if (!node) return;\n    const update = () => setContentHeight(node.offsetHeight);\n    update();\n    const observer = new ResizeObserver(update);\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, []);\n\n  if (variant === \"quiet\") {\n    return (\n      <motion.div\n        layout={!reduce}\n        transition={{ layout: SPRING_LAYOUT }}\n        data-slot=\"agent-inbox-item\"\n        data-variant=\"quiet\"\n        className={cn(\n          \"flex min-h-12 flex-wrap items-center gap-3 px-0 py-2.5 text-sm\",\n          className,\n        )}\n      >\n        <div className=\"flex min-w-0 basis-[34%] items-center gap-2 font-medium\">\n          {icon ? (\n            <span className=\"flex h-4 w-4 shrink-0 items-center justify-center text-muted-foreground\">\n              {icon}\n            </span>\n          ) : null}\n          <span className=\"truncate\">{title}</span>\n        </div>\n        <div className=\"min-w-0 flex-1 text-muted-foreground\">\n          {source ? <div className=\"truncate text-[11px]\">{source}</div> : null}\n          {description ? (\n            <div className=\"truncate text-[12px]\">{description}</div>\n          ) : null}\n        </div>\n        <div className=\"ml-auto flex shrink-0 items-center gap-1.5\">\n          {risk ? <InboxRiskBadge risk={risk} /> : null}\n          {expires ? (\n            <span className=\"text-[11px] text-muted-foreground/70 tabular-nums\">\n              {expires}\n            </span>\n          ) : null}\n          {pending ? (\n            <>\n              <InboxActionButton variant=\"ghost\" onClick={onDeny}>\n                {denyLabel}\n              </InboxActionButton>\n              <InboxActionButton variant=\"primary\" onClick={onApprove}>\n                {approveLabel}\n              </InboxActionButton>\n            </>\n          ) : resolution !== undefined ? (\n            <span\n              className={cn(\n                \"text-[12px]\",\n                status === \"approved\" && \"text-[var(--wb-success)]\",\n                status === \"denied\" && \"text-muted-foreground\",\n                status === \"expired\" && \"text-muted-foreground/60 italic\",\n              )}\n            >\n              {resolution}\n            </span>\n          ) : status === \"approved\" ? (\n            <Check className=\"h-4 w-4 text-[var(--wb-success)]\" />\n          ) : null}\n        </div>\n        {hasDetails ? (\n          <div className=\"w-full pl-6\">\n            <button\n              type=\"button\"\n              aria-expanded={detailsOpen}\n              onClick={() => setDetailsOpen((open) => !open)}\n              className=\"-mx-1 inline-flex items-center gap-1 rounded-md px-1 text-muted-foreground text-xs transition-colors hover:bg-[var(--wb-hover)]\"\n            >\n              Details\n              <ChevronRight className=\"h-3 w-3\" />\n            </button>\n            {detailsOpen ? <div className=\"pt-2\">{children}</div> : null}\n          </div>\n        ) : null}\n      </motion.div>\n    );\n  }\n\n  return (\n    <motion.div\n      layout={!reduce}\n      transition={{ layout: SPRING_LAYOUT }}\n      data-slot=\"agent-inbox-item\"\n      data-variant=\"card\"\n      className={cn(\"px-4 py-3\", className)}\n    >\n      <div className={cn(!pending && \"opacity-70\")}>\n        <div className=\"flex items-center gap-2\">\n          {icon ? (\n            <span className=\"flex h-4 w-4 shrink-0 items-center justify-center text-muted-foreground\">\n              {icon}\n            </span>\n          ) : null}\n          {source ? <span className=\"text-xs text-muted-foreground\">{source}</span> : null}\n          {risk ? <InboxRiskBadge risk={risk} /> : null}\n          {expires ? (\n            <span className=\"ml-auto text-xs text-muted-foreground/70 tabular-nums\">{expires}</span>\n          ) : null}\n        </div>\n        <div className=\"mt-1.5 font-medium text-sm\">{title}</div>\n        {description ? <div className=\"text-[13px] text-muted-foreground\">{description}</div> : null}\n        {hasDetails ? (\n          <>\n            <button\n              type=\"button\"\n              aria-expanded={detailsOpen}\n              onClick={() => setDetailsOpen((open) => !open)}\n              className=\"-mx-1 mt-1 inline-flex items-center gap-1 self-start rounded-md px-1 text-muted-foreground text-xs transition-colors hover:bg-[var(--wb-hover)]\"\n            >\n              Details\n              <motion.span\n                aria-hidden\n                className=\"flex text-muted-foreground/60\"\n                animate={reduce ? undefined : { rotate: detailsOpen ? 90 : 0 }}\n                style={reduce ? { transform: detailsOpen ? \"rotate(90deg)\" : \"rotate(0deg)\" } : undefined}\n                transition={reduce ? undefined : { duration: 0.2, ease: EASE_OUT }}\n              >\n                <ChevronRight className=\"h-3 w-3\" />\n              </motion.span>\n            </button>\n            <motion.div\n              initial={false}\n              animate={reduce ? undefined : { height: detailsOpen ? contentHeight : 0 }}\n              transition={{ duration: 0.25, ease: EASE_OUT }}\n              className={cn(\"overflow-hidden\", reduce && (detailsOpen ? \"h-auto\" : \"h-0\"))}\n            >\n              <div ref={contentRef} className=\"pt-2\">\n                {children}\n              </div>\n            </motion.div>\n          </>\n        ) : null}\n      </div>\n      {pending ? (\n        <div className=\"mt-2 flex items-center gap-1.5\">\n          <InboxActionButton variant=\"ghost\" onClick={onDeny}>\n            {denyLabel}\n          </InboxActionButton>\n          <InboxActionButton variant=\"primary\" onClick={onApprove}>\n            {approveLabel}\n          </InboxActionButton>\n        </div>\n      ) : resolution !== undefined ? (\n        <div\n          className={cn(\n            \"mt-2 text-[13px]\",\n            status === \"approved\" && \"text-[var(--wb-success)]\",\n            status === \"denied\" && \"text-muted-foreground\",\n            status === \"expired\" && \"text-muted-foreground/60 italic\",\n          )}\n        >\n          {resolution}\n        </div>\n      ) : null}\n    </motion.div>\n  );\n}\n\nexport interface ActionReceiptProps {\n  /** 16px icon, defaults to a `Check` in the library's success green. */\n  icon?: ReactNode;\n  title: ReactNode;\n  /** Sub-line describing what was acted on, e.g. \"db: production/users, sessions\". */\n  scope?: ReactNode;\n  added?: number;\n  removed?: number;\n  timestamp?: ReactNode;\n  onUndo?: () => void;\n  undoLabel?: ReactNode;\n  className?: string;\n  /** Optional extra content, e.g. a list of affected file rows. */\n  children?: ReactNode;\n}\n\n/**\n * Operation receipt — the record of a completed action, usable standalone or\n * dropped into an `InboxItem`'s detail region (e.g. a \"planned changes\"\n * preview before approval, or a completion summary after). A hairline-ringed\n * surface (the `ThreadCard` box-shadow technique) rather than `AgentInbox`'s\n * full card chrome, since it's meant to nest inside other rows.\n */\nexport function ActionReceipt({\n  icon,\n  title,\n  scope,\n  added,\n  removed,\n  timestamp,\n  onUndo,\n  undoLabel = \"Undo\",\n  className,\n  children,\n}: ActionReceiptProps) {\n  return (\n    <div\n      style={{ boxShadow: \"0 0 0 0.5px var(--wb-hairline)\" }}\n      className={cn(\n        \"rounded-xl bg-[var(--wb-inset-faint)] p-3\",\n        className,\n      )}\n    >\n      <div className=\"flex items-center gap-2\">\n        <span className=\"flex h-4 w-4 shrink-0 items-center justify-center text-[var(--wb-success)]\">\n          {icon ?? <Check className=\"h-4 w-4\" />}\n        </span>\n        <span className=\"font-medium text-sm\">{title}</span>\n        {timestamp ? (\n          <span className=\"ml-auto text-muted-foreground/70 text-xs\">{timestamp}</span>\n        ) : null}\n      </div>\n      {scope ? <div className=\"mt-1 text-[13px] text-muted-foreground\">{scope}</div> : null}\n      {added !== undefined || removed !== undefined ? (\n        <div className=\"mt-1 flex gap-1.5 text-[13px]\">\n          {added !== undefined ? (\n            <span className=\"text-[var(--wb-success)]\">+{added}</span>\n          ) : null}\n          {removed !== undefined ? (\n            <span className=\"text-[var(--wb-danger)]\">−{removed}</span>\n          ) : null}\n        </div>\n      ) : null}\n      {children}\n      {onUndo ? (\n        <div className=\"mt-2\">\n          <button\n            type=\"button\"\n            onClick={onUndo}\n            className=\"flex h-6 items-center rounded-md px-2 text-muted-foreground text-xs transition-colors hover:bg-[var(--wb-hover)] hover:text-foreground\"\n          >\n            {undoLabel}\n          </button>\n        </div>\n      ) : null}\n    </div>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}