{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"agent-workbench","type":"registry:block","title":"Agent Workbench","description":"Responsive agent app shell: a resizable three-pane desktop layout, tablet side overlays, and mobile single-surface navigation, with spring transitions and a full-width 46px toolbar overlay. Includes a pinned summary card overlay.","author":"UI Lab","dependencies":["clsx","motion","react-use-measure","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-workbench/index.tsx","type":"registry:component","target":"@components/motion/agent-workbench/index.tsx","content":"\"use client\";\n// ui-lab-ten.vercel.app/components/blocks/agent-workbench\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport {\n  createContext,\n  type ReactNode,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport useMeasure from \"react-use-measure\";\nimport { EASE_OUT, EASE_OUT_CSS, SPRING_PANEL } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\nimport { ResizeHandle } from \"./resize-handle\";\n\n/** Height of the overlay toolbar (`WorkbenchHeader`), in pixels. */\nexport const HEADER_HEIGHT = 46;\n/** Sidebar width when `defaultSidebarWidth` is unset, and its reset target. */\nexport const SIDEBAR_DEFAULT_WIDTH = 275;\nexport const SIDEBAR_MIN_WIDTH = 240;\nexport const SIDEBAR_MAX_WIDTH = 520;\n/** Panel width when `defaultPanelWidth` is unset, and its reset target. */\nexport const PANEL_DEFAULT_WIDTH = 384;\nexport const PANEL_MIN_WIDTH = 320;\n/** Floor kept for the main column — sidebar/panel can never squeeze past it. */\nexport const MAIN_MIN_WIDTH = 320;\n\n// Static ceiling used for the panel's aria-valuemax before the container has\n// been measured (first paint only — react-use-measure reports 0 until its\n// ResizeObserver fires).\nconst PANEL_MAX_FALLBACK = 960;\n\nexport type WorkbenchLayoutMode = \"desktop\" | \"tablet\" | \"mobile\";\n\n// Drag (or arrow-key nudge) a pane this far past its own minimum and it\n// collapses instead of clamping at the minimum — the \"drag past the edge to\n// close\" gesture native three-pane app shells use.\nconst COLLAPSE_MARGIN = 40;\n\nconst clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n\ninterface WorkbenchContextValue {\n  layoutMode: WorkbenchLayoutMode;\n  sidebarOpen: boolean;\n  panelOpen: boolean;\n  toggleSidebar: () => void;\n  togglePanel: () => void;\n  setSidebarOpen: (open: boolean) => void;\n  setPanelOpen: (open: boolean) => void;\n  sidebarWidth: number;\n  panelWidth: number;\n  // Internal wiring for WorkbenchSidebar / WorkbenchPanel / WorkbenchHeader.\n  // Not part of the public useWorkbench() contract.\n  containerWidth: number;\n  sidebarDefault: number;\n  panelDefault: number;\n  sidebarDragging: boolean;\n  panelDragging: boolean;\n  setSidebarDragging: (dragging: boolean) => void;\n  setPanelDragging: (dragging: boolean) => void;\n  resizeSidebar: (rawWidth: number) => void;\n  resizePanel: (rawWidth: number) => void;\n  resetSidebarWidth: () => void;\n  resetPanelWidth: () => void;\n}\n\nconst WorkbenchContext = createContext<WorkbenchContextValue | null>(null);\n\nfunction useWorkbenchInternal(component: string): WorkbenchContextValue {\n  const ctx = useContext(WorkbenchContext);\n  if (!ctx) throw new Error(`${component} must be used inside <Workbench>`);\n  return ctx;\n}\n\n/** Reads the workbench's open/close and width state. Must be used inside `<Workbench>`. */\nexport function useWorkbench() {\n  const {\n    layoutMode,\n    sidebarOpen,\n    panelOpen,\n    toggleSidebar,\n    togglePanel,\n    setSidebarOpen,\n    setPanelOpen,\n    sidebarWidth,\n    panelWidth,\n  } = useWorkbenchInternal(\"useWorkbench\");\n  return {\n    layoutMode,\n    sidebarOpen,\n    panelOpen,\n    toggleSidebar,\n    togglePanel,\n    setSidebarOpen,\n    setPanelOpen,\n    sidebarWidth,\n    panelWidth,\n  };\n}\n\nexport interface WorkbenchProps {\n  sidebarOpen?: boolean;\n  defaultSidebarOpen?: boolean;\n  onSidebarOpenChange?: (open: boolean) => void;\n  panelOpen?: boolean;\n  defaultPanelOpen?: boolean;\n  onPanelOpenChange?: (open: boolean) => void;\n  /** Initial sidebar width, and its double-click/Enter reset target. Defaults to `SIDEBAR_DEFAULT_WIDTH`. */\n  defaultSidebarWidth?: number;\n  /** Initial panel width, and its double-click/Enter reset target. Defaults to `PANEL_DEFAULT_WIDTH`. */\n  defaultPanelWidth?: number;\n  /** Width at which the inline three-column layout begins. Defaults to 1200px. */\n  desktopBreakpoint?: number;\n  /** Width below which the workbench shows one task surface at a time. Defaults to 720px. */\n  tabletBreakpoint?: number;\n  className?: string;\n  children?: ReactNode;\n}\n\n/**\n * Three-pane app shell root: a resizable sidebar, a flexible main column and\n * a resizable utility panel, laid out as a single flex row. Compose it with\n * `WorkbenchHeader` (an absolutely-positioned overlay), `WorkbenchSidebar`,\n * `WorkbenchMain` and `WorkbenchPanel` as children, in whatever DOM order —\n * only the header's position is order-independent by design.\n */\nexport function Workbench({\n  sidebarOpen: sidebarOpenProp,\n  defaultSidebarOpen = true,\n  onSidebarOpenChange,\n  panelOpen: panelOpenProp,\n  defaultPanelOpen = false,\n  onPanelOpenChange,\n  defaultSidebarWidth,\n  defaultPanelWidth,\n  desktopBreakpoint = 1200,\n  tabletBreakpoint = 720,\n  className,\n  children,\n}: WorkbenchProps) {\n  const [containerRef, bounds] = useMeasure();\n  const containerWidth = bounds.width;\n  const layoutMode: WorkbenchLayoutMode =\n    containerWidth > 0 && containerWidth < tabletBreakpoint\n      ? \"mobile\"\n      : containerWidth > 0 && containerWidth < desktopBreakpoint\n        ? \"tablet\"\n        : \"desktop\";\n\n  const sidebarDefault = defaultSidebarWidth ?? SIDEBAR_DEFAULT_WIDTH;\n  const panelDefault = defaultPanelWidth ?? PANEL_DEFAULT_WIDTH;\n\n  const [internalSidebarOpen, setInternalSidebarOpen] = useState(defaultSidebarOpen);\n  const [internalPanelOpen, setInternalPanelOpen] = useState(defaultPanelOpen);\n  const sidebarControlled = sidebarOpenProp !== undefined;\n  const panelControlled = panelOpenProp !== undefined;\n  const sidebarOpen = sidebarControlled ? sidebarOpenProp : internalSidebarOpen;\n  const panelOpen = panelControlled ? panelOpenProp : internalPanelOpen;\n\n  // Which side pane the user opened last — the other one yields when the\n  // desktop container can't fit both (see the auto-yield effect below).\n  const lastOpenedRef = useRef<\"sidebar\" | \"panel\">(\"sidebar\");\n\n  const setSidebarOpen = useCallback(\n    (open: boolean) => {\n      if (open) lastOpenedRef.current = \"sidebar\";\n      if (open && layoutMode !== \"desktop\") {\n        if (!panelControlled) setInternalPanelOpen(false);\n        onPanelOpenChange?.(false);\n      }\n      if (!sidebarControlled) setInternalSidebarOpen(open);\n      onSidebarOpenChange?.(open);\n    },\n    [\n      layoutMode,\n      panelControlled,\n      onPanelOpenChange,\n      sidebarControlled,\n      onSidebarOpenChange,\n    ],\n  );\n  const setPanelOpen = useCallback(\n    (open: boolean) => {\n      if (open) lastOpenedRef.current = \"panel\";\n      if (open && layoutMode !== \"desktop\") {\n        if (!sidebarControlled) setInternalSidebarOpen(false);\n        onSidebarOpenChange?.(false);\n      }\n      if (!panelControlled) setInternalPanelOpen(open);\n      onPanelOpenChange?.(open);\n    },\n    [\n      layoutMode,\n      sidebarControlled,\n      onSidebarOpenChange,\n      panelControlled,\n      onPanelOpenChange,\n    ],\n  );\n  const toggleSidebar = useCallback(\n    () => setSidebarOpen(!sidebarOpen),\n    [setSidebarOpen, sidebarOpen],\n  );\n  const togglePanel = useCallback(() => setPanelOpen(!panelOpen), [setPanelOpen, panelOpen]);\n\n  const [sidebarWidth, setSidebarWidth] = useState(sidebarDefault);\n  const [panelWidth, setPanelWidth] = useState(panelDefault);\n  const [sidebarDragging, setSidebarDragging] = useState(false);\n  const [panelDragging, setPanelDragging] = useState(false);\n\n  const resizeSidebar = useCallback(\n    (rawWidth: number) => {\n      if (rawWidth < SIDEBAR_MIN_WIDTH - COLLAPSE_MARGIN) {\n        setSidebarOpen(false);\n        return;\n      }\n      const spaceLimit =\n        containerWidth > 0 ? containerWidth - MAIN_MIN_WIDTH : SIDEBAR_MAX_WIDTH;\n      const max = Math.max(SIDEBAR_MIN_WIDTH, Math.min(SIDEBAR_MAX_WIDTH, spaceLimit));\n      setSidebarWidth(clamp(rawWidth, SIDEBAR_MIN_WIDTH, max));\n    },\n    [setSidebarOpen, containerWidth],\n  );\n\n  const resizePanel = useCallback(\n    (rawWidth: number) => {\n      if (rawWidth < PANEL_MIN_WIDTH - COLLAPSE_MARGIN) {\n        setPanelOpen(false);\n        return;\n      }\n      const reserved = sidebarOpen ? sidebarWidth : 0;\n      const max =\n        containerWidth > 0\n          ? Math.max(PANEL_MIN_WIDTH, containerWidth - reserved - MAIN_MIN_WIDTH)\n          : PANEL_MAX_FALLBACK;\n      setPanelWidth(clamp(rawWidth, PANEL_MIN_WIDTH, max));\n    },\n    [setPanelOpen, sidebarOpen, sidebarWidth, containerWidth],\n  );\n\n  const resetSidebarWidth = useCallback(() => setSidebarWidth(sidebarDefault), [sidebarDefault]);\n  const resetPanelWidth = useCallback(() => setPanelWidth(panelDefault), [panelDefault]);\n\n  const previousLayoutModeRef = useRef<WorkbenchLayoutMode>(layoutMode);\n\n  // A narrow workbench starts with its navigation out of the way. This is an\n  // intentional mode transition rather than a width-fitting fallback: the\n  // panel remains open when an application controls it, and can be shown as an\n  // overlay without squeezing the task surface.\n  useEffect(() => {\n    const previous = previousLayoutModeRef.current;\n    previousLayoutModeRef.current = layoutMode;\n    if (layoutMode === \"desktop\" || previous === layoutMode) return;\n    setSidebarOpen(false);\n  }, [layoutMode, setSidebarOpen]);\n\n  // Re-clamp (never collapse) whenever the measured container shrinks, so a\n  // narrower window can't leave the sidebar/panel wider than there's room for.\n  useEffect(() => {\n    if (containerWidth <= 0 || layoutMode !== \"desktop\") return;\n    setSidebarWidth((w) =>\n      clamp(w, SIDEBAR_MIN_WIDTH, Math.max(SIDEBAR_MIN_WIDTH, containerWidth - MAIN_MIN_WIDTH)),\n    );\n  }, [containerWidth, layoutMode]);\n\n  useEffect(() => {\n    if (containerWidth <= 0 || layoutMode !== \"desktop\") return;\n    const reserved = sidebarOpen ? sidebarWidth : 0;\n    setPanelWidth((w) =>\n      clamp(w, PANEL_MIN_WIDTH, Math.max(PANEL_MIN_WIDTH, containerWidth - reserved - MAIN_MIN_WIDTH)),\n    );\n  }, [containerWidth, layoutMode, sidebarOpen, sidebarWidth]);\n\n  // Both side panes plus the main floor can exceed a narrow container even at\n  // their minimum widths. When they do, the pane opened less recently yields —\n  // the narrow-window rule desktop three-pane shells use — so opening one side\n  // swaps the other out instead of crushing the main column.\n  useEffect(() => {\n    if (layoutMode !== \"desktop\" || containerWidth <= 0 || !sidebarOpen || !panelOpen) return;\n    if (sidebarWidth + panelWidth + MAIN_MIN_WIDTH <= containerWidth) return;\n    if (lastOpenedRef.current === \"panel\") setSidebarOpen(false);\n    else setPanelOpen(false);\n  }, [layoutMode, containerWidth, sidebarOpen, panelOpen, sidebarWidth, panelWidth, setSidebarOpen, setPanelOpen]);\n\n  const value = useMemo<WorkbenchContextValue>(\n    () => ({\n      layoutMode,\n      sidebarOpen,\n      panelOpen,\n      toggleSidebar,\n      togglePanel,\n      setSidebarOpen,\n      setPanelOpen,\n      sidebarWidth,\n      panelWidth,\n      containerWidth,\n      sidebarDefault,\n      panelDefault,\n      sidebarDragging,\n      panelDragging,\n      setSidebarDragging,\n      setPanelDragging,\n      resizeSidebar,\n      resizePanel,\n      resetSidebarWidth,\n      resetPanelWidth,\n    }),\n    [\n      layoutMode,\n      sidebarOpen,\n      panelOpen,\n      toggleSidebar,\n      togglePanel,\n      setSidebarOpen,\n      setPanelOpen,\n      sidebarWidth,\n      panelWidth,\n      containerWidth,\n      sidebarDefault,\n      panelDefault,\n      sidebarDragging,\n      panelDragging,\n      resizeSidebar,\n      resizePanel,\n      resetSidebarWidth,\n      resetPanelWidth,\n    ],\n  );\n\n  return (\n    <WorkbenchContext.Provider value={value}>\n      <div\n        ref={containerRef}\n        data-layout-mode={layoutMode}\n        className={cn(\"relative isolate flex h-full min-h-0 w-full overflow-hidden\", className)}\n      >\n        {children}\n      </div>\n    </WorkbenchContext.Provider>\n  );\n}\n\nexport interface WorkbenchSidebarProps {\n  className?: string;\n  children?: ReactNode;\n}\n\n/** Translucent, resizable left pane. Drag its right-edge handle past the\n * minimum to collapse it; double-click the handle to reset its width. */\nexport function WorkbenchSidebar({ className, children }: WorkbenchSidebarProps) {\n  const {\n    layoutMode,\n    sidebarOpen,\n    sidebarWidth,\n    sidebarDragging,\n    containerWidth,\n    resizeSidebar,\n    resetSidebarWidth,\n    setSidebarDragging,\n    setSidebarOpen,\n  } = useWorkbenchInternal(\"WorkbenchSidebar\");\n  const reduce = useReducedMotion() ?? false;\n  const ariaMax =\n    containerWidth > 0\n      ? Math.max(SIDEBAR_MIN_WIDTH, Math.min(SIDEBAR_MAX_WIDTH, containerWidth - MAIN_MIN_WIDTH))\n      : SIDEBAR_MAX_WIDTH;\n\n  if (layoutMode !== \"desktop\") {\n    const mobile = layoutMode === \"mobile\";\n    return (\n      <>\n        {sidebarOpen ? (\n          <button\n            type=\"button\"\n            aria-label=\"Close sidebar overlay\"\n            className=\"absolute inset-x-0 bottom-0 top-[46px] z-10 cursor-default bg-[var(--wb-overlay-scrim)]\"\n            onClick={() => setSidebarOpen(false)}\n          />\n        ) : null}\n        <motion.aside\n          inert={!sidebarOpen}\n          aria-label=\"Navigation overlay\"\n          initial={false}\n          animate={{ x: sidebarOpen ? \"0%\" : \"-100%\", opacity: sidebarOpen ? 1 : 0 }}\n          transition={{ x: reduce ? { duration: 0 } : SPRING_PANEL, opacity: reduce ? { duration: 0.15, ease: EASE_OUT } : SPRING_PANEL }}\n          className={cn(\n            \"absolute inset-y-0 left-0 z-20 overflow-hidden bg-[var(--wb-surface-translucent)] backdrop-blur-xl\",\n            mobile ? \"w-full\" : \"max-w-[min(86vw,520px)]\",\n            className,\n          )}\n          style={mobile ? undefined : { width: sidebarWidth }}\n        >\n          <div className=\"h-full overflow-y-auto pt-[46px]\">{children}</div>\n        </motion.aside>\n      </>\n    );\n  }\n\n  return (\n    <motion.aside\n      inert={!sidebarOpen}\n      initial={false}\n      animate={{ width: sidebarOpen ? sidebarWidth : 0, opacity: sidebarOpen ? 1 : 0 }}\n      transition={{\n        width: sidebarDragging || reduce ? { duration: 0 } : SPRING_PANEL,\n        opacity: reduce ? { duration: 0.15, ease: EASE_OUT } : SPRING_PANEL,\n      }}\n      className={cn(\n        // overflow stays visible so the resize handle can straddle the edge\n        // (z-10 keeps that overhang hit-testable above the main column);\n        // clipping happens one div down.\n        \"relative isolate z-10 shrink-0 overflow-visible bg-[var(--wb-surface-translucent)] backdrop-blur-xl\",\n        className,\n      )}\n    >\n      <div className=\"absolute inset-0 overflow-hidden\">\n        {/* Content is laid out at the pane's resting width so open/close only\n            clips it instead of re-wrapping every line mid-animation. */}\n        <div\n          className=\"h-full overflow-y-auto pt-[46px] [mask-image:linear-gradient(to_bottom,transparent_0,black_16px,black_calc(100%-24px),transparent_100%)]\"\n          style={{ width: sidebarWidth }}\n        >\n          {children}\n        </div>\n      </div>\n      {sidebarOpen ? (\n        <ResizeHandle\n          edge=\"right\"\n          value={sidebarWidth}\n          min={SIDEBAR_MIN_WIDTH}\n          max={ariaMax}\n          onResize={resizeSidebar}\n          onReset={resetSidebarWidth}\n          onDraggingChange={setSidebarDragging}\n          aria-label=\"Resize sidebar\"\n        />\n      ) : null}\n    </motion.aside>\n  );\n}\n\nexport interface WorkbenchMainProps {\n  className?: string;\n  children?: ReactNode;\n}\n\n/** Opaque, flexible main column — takes up whatever width the sidebar/panel leave behind. */\nexport function WorkbenchMain({ className, children }: WorkbenchMainProps) {\n  const { layoutMode, sidebarOpen, panelOpen } = useWorkbenchInternal(\"WorkbenchMain\");\n  return (\n    <main\n      inert={layoutMode !== \"desktop\" && (sidebarOpen || panelOpen)}\n      className={cn(\n        \"relative flex min-h-0 min-w-0 flex-1 flex-col bg-[var(--wb-surface)]\",\n        className,\n      )}\n    >\n      <div className=\"flex min-h-0 flex-1 flex-col pt-[46px]\">{children}</div>\n    </main>\n  );\n}\n\nexport interface WorkbenchPanelProps {\n  className?: string;\n  children?: ReactNode;\n}\n\n/** Opaque, resizable right pane, squeezed in-flow (not floated). Drag its\n * left-edge handle past the minimum to collapse it; double-click to reset. */\nexport function WorkbenchPanel({ className, children }: WorkbenchPanelProps) {\n  const {\n    layoutMode,\n    panelOpen,\n    panelWidth,\n    panelDragging,\n    sidebarOpen,\n    sidebarWidth,\n    containerWidth,\n    resizePanel,\n    resetPanelWidth,\n    setPanelDragging,\n    setPanelOpen,\n  } = useWorkbenchInternal(\"WorkbenchPanel\");\n  const reduce = useReducedMotion() ?? false;\n  const reserved = sidebarOpen ? sidebarWidth : 0;\n  const ariaMax =\n    containerWidth > 0\n      ? Math.max(PANEL_MIN_WIDTH, containerWidth - reserved - MAIN_MIN_WIDTH)\n      : PANEL_MAX_FALLBACK;\n\n  if (layoutMode !== \"desktop\") {\n    const mobile = layoutMode === \"mobile\";\n    return (\n      <>\n        {panelOpen ? (\n          <button\n            type=\"button\"\n            aria-label=\"Close artifact overlay\"\n            className=\"absolute inset-x-0 bottom-0 top-[46px] z-10 cursor-default bg-[var(--wb-overlay-scrim)]\"\n            onClick={() => setPanelOpen(false)}\n          />\n        ) : null}\n        <motion.aside\n          inert={!panelOpen}\n          aria-label=\"Artifact overlay\"\n          initial={false}\n          animate={{ x: panelOpen ? \"0%\" : \"100%\", opacity: panelOpen ? 1 : 0 }}\n          transition={{ x: reduce ? { duration: 0 } : SPRING_PANEL, opacity: reduce ? { duration: 0.15, ease: EASE_OUT } : SPRING_PANEL }}\n          className={cn(\n            \"absolute inset-y-0 right-0 z-20 overflow-hidden border-[var(--wb-border-subtle)] border-l bg-[var(--wb-surface)]\",\n            mobile ? \"w-full\" : \"max-w-[min(86vw,560px)]\",\n            className,\n          )}\n          style={mobile ? undefined : { width: panelWidth }}\n        >\n          <div className=\"h-full overflow-y-auto pt-[46px]\">{children}</div>\n        </motion.aside>\n      </>\n    );\n  }\n\n  return (\n    <motion.aside\n      inert={!panelOpen}\n      initial={false}\n      animate={{ width: panelOpen ? panelWidth : 0, opacity: panelOpen ? 1 : 0 }}\n      transition={{\n        width: panelDragging || reduce ? { duration: 0 } : SPRING_PANEL,\n        opacity: reduce ? { duration: 0.15, ease: EASE_OUT } : SPRING_PANEL,\n      }}\n      className={cn(\n        \"relative isolate z-10 shrink-0 overflow-visible border-[var(--wb-border-subtle)] border-l bg-[var(--wb-surface)]\",\n        className,\n      )}\n    >\n      <div className=\"absolute inset-0 overflow-hidden\">\n        <div className=\"h-full overflow-y-auto pt-[46px]\" style={{ width: panelWidth }}>\n          {children}\n        </div>\n      </div>\n      {panelOpen ? (\n        <ResizeHandle\n          edge=\"left\"\n          value={panelWidth}\n          min={PANEL_MIN_WIDTH}\n          max={ariaMax}\n          onResize={resizePanel}\n          onReset={resetPanelWidth}\n          onDraggingChange={setPanelDragging}\n          aria-label=\"Resize panel\"\n        />\n      ) : null}\n    </motion.aside>\n  );\n}\n\nexport interface WorkbenchHeaderProps {\n  /** Rendered above the sidebar; its wrapper width tracks `sidebarWidth` while open. */\n  leading?: ReactNode;\n  trailing?: ReactNode;\n  children?: ReactNode;\n  className?: string;\n}\n\n/** Full-width, 46px overlay toolbar. Absolutely positioned so it sits above\n * the sidebar/main/panel row regardless of where it's placed in the DOM. */\nexport function WorkbenchHeader({ leading, trailing, children, className }: WorkbenchHeaderProps) {\n  const { layoutMode, sidebarOpen, sidebarWidth, sidebarDragging } = useWorkbenchInternal(\"WorkbenchHeader\");\n  const reduce = useReducedMotion() ?? false;\n\n  return (\n    <header\n      className={cn(\n        \"pointer-events-none absolute inset-x-0 top-0 z-30 flex h-[46px] items-center\",\n        className,\n      )}\n    >\n      <div\n        className=\"pointer-events-auto flex h-full shrink-0 items-center overflow-hidden\"\n        style={{\n          width: layoutMode === \"desktop\" && sidebarOpen ? sidebarWidth : \"auto\",\n          transitionProperty: \"width\",\n          transitionDuration: sidebarDragging || reduce ? \"0ms\" : \"300ms\",\n          transitionTimingFunction: EASE_OUT_CSS,\n        }}\n      >\n        {leading}\n      </div>\n      <div className=\"pointer-events-auto min-w-0 flex-1\">{children}</div>\n      <div className=\"pointer-events-auto flex shrink-0 items-center\">{trailing}</div>\n    </header>\n  );\n}\n\nexport { WorkbenchSummaryCard, WorkbenchSummarySection } from \"./summary-card\";\n"},{"path":"components/motion/agent-workbench/resize-handle.tsx","type":"registry:component","target":"@components/motion/agent-workbench/resize-handle.tsx","content":"\"use client\";\n// ui-lab-ten.vercel.app/components/blocks/agent-workbench\n\nimport {\n  type KeyboardEvent,\n  type PointerEvent,\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface ResizeHandleProps {\n  /** Which edge of the pane this handle sits on — sets both the hit-area\n   * position and the sign of the pointer delta (dragging toward the pane\n   * grows it, away from it shrinks it). */\n  edge: \"left\" | \"right\";\n  /** Current pane width, used for keyboard math and the aria value. */\n  value: number;\n  min: number;\n  max: number;\n  /** Raw (unclamped) next width — the caller owns clamping/collapse. */\n  onResize: (nextValue: number) => void;\n  /** Called on double-click or Enter — the caller owns what \"default\" means. */\n  onReset: () => void;\n  onDraggingChange?: (dragging: boolean) => void;\n  className?: string;\n  \"aria-label\"?: string;\n}\n\nconst KEY_STEP = 16;\n\nexport function ResizeHandle({\n  edge,\n  value,\n  min,\n  max,\n  onResize,\n  onReset,\n  onDraggingChange,\n  className,\n  \"aria-label\": ariaLabel,\n}: ResizeHandleProps) {\n  const [dragging, setDragging] = useState(false);\n  // Captured at drag start so pointer math is relative to where the drag\n  // began, not the (already-updating) live value.\n  const dragStartRef = useRef({ pointerX: 0, width: value });\n  const valueRef = useRef(value);\n  valueRef.current = value;\n\n  const setDraggingState = useCallback(\n    (next: boolean) => {\n      setDragging(next);\n      onDraggingChange?.(next);\n    },\n    [onDraggingChange],\n  );\n\n  // A drag past the collapse threshold unmounts this handle mid-gesture (the\n  // pane conditionally renders it), so no pointerup ever fires — clear the\n  // parent's dragging flag on unmount or every later open/close animation\n  // would keep the zeroed drag transition.\n  const onDraggingChangeRef = useRef(onDraggingChange);\n  onDraggingChangeRef.current = onDraggingChange;\n  useEffect(() => () => onDraggingChangeRef.current?.(false), []);\n\n  const onPointerDown = useCallback(\n    (event: PointerEvent<HTMLDivElement>) => {\n      event.currentTarget.setPointerCapture(event.pointerId);\n      dragStartRef.current = { pointerX: event.clientX, width: valueRef.current };\n      setDraggingState(true);\n    },\n    [setDraggingState],\n  );\n\n  const onPointerMove = useCallback(\n    (event: PointerEvent<HTMLDivElement>) => {\n      if (!dragging) return;\n      const delta = event.clientX - dragStartRef.current.pointerX;\n      const signedDelta = edge === \"right\" ? delta : -delta;\n      onResize(dragStartRef.current.width + signedDelta);\n    },\n    [dragging, edge, onResize],\n  );\n\n  const endDrag = useCallback(\n    (event: PointerEvent<HTMLDivElement>) => {\n      event.currentTarget.releasePointerCapture?.(event.pointerId);\n      setDraggingState(false);\n    },\n    [setDraggingState],\n  );\n\n  const onKeyDown = useCallback(\n    (event: KeyboardEvent<HTMLDivElement>) => {\n      if (event.key === \"Enter\") {\n        event.preventDefault();\n        onReset();\n        return;\n      }\n      // Arrows move the divider, not the width: on a left-edge handle the\n      // divider sits left of the pane, so ArrowRight shrinks the pane.\n      const dir = edge === \"right\" ? 1 : -1;\n      const map: Record<string, number> = {\n        ArrowLeft: value - KEY_STEP * dir,\n        ArrowRight: value + KEY_STEP * dir,\n        Home: min,\n        End: max,\n      };\n      const next = map[event.key];\n      if (next !== undefined) {\n        event.preventDefault();\n        onResize(next);\n      }\n    },\n    [edge, value, min, max, onResize, onReset],\n  );\n\n  return (\n    // biome-ignore lint/a11y/useSemanticElements: WAI-ARIA \"window splitter\" pattern — a focusable, draggable separator; <hr> can't take the child gradient line or own drag/keyboard handlers.\n    <div\n      role=\"separator\"\n      aria-orientation=\"vertical\"\n      aria-valuenow={Math.round(value)}\n      aria-valuemin={min}\n      aria-valuemax={max}\n      aria-label={ariaLabel}\n      tabIndex={0}\n      onPointerDown={onPointerDown}\n      onPointerMove={onPointerMove}\n      onPointerUp={endDrag}\n      onPointerCancel={endDrag}\n      onKeyDown={onKeyDown}\n      onDoubleClick={onReset}\n      className={cn(\n        \"group absolute inset-y-0 z-10 w-4 touch-none cursor-col-resize select-none outline-none\",\n        edge === \"right\" ? \"right-0 translate-x-2\" : \"left-0 -translate-x-2\",\n        className,\n      )}\n    >\n      <div\n        className={cn(\n          \"mx-auto h-full w-px bg-gradient-to-b from-transparent via-[var(--wb-resize-handle)] to-transparent opacity-0 transition-opacity\",\n          \"group-hover:opacity-100 group-focus-visible:opacity-100\",\n          dragging && \"opacity-100\",\n        )}\n      />\n    </div>\n  );\n}\n"},{"path":"components/motion/agent-workbench/summary-card.tsx","type":"registry:component","target":"@components/motion/agent-workbench/summary-card.tsx","content":"\"use client\";\n// ui-lab-ten.vercel.app/components/blocks/agent-workbench\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport type { ReactNode } from \"react\";\nimport { EASE_OUT, SPRING_PANEL } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface WorkbenchSummaryCardProps {\n  /** Controlled open state — mounts/unmounts through `AnimatePresence`. */\n  open: boolean;\n  className?: string;\n  children?: ReactNode;\n}\n\n/**\n * Pinned summary card overlay — a floating panel anchored to the top-right\n * corner, meant for at-a-glance session/environment info that stays visible\n * above the thread. Must be rendered inside `<WorkbenchMain>` (its `relative`\n * ancestor); it does not read from `useWorkbench()` and is unaware of\n * sidebar/panel state.\n *\n * `top-[54px]` = the 46px `HEADER_HEIGHT` toolbar overlay plus an 8px gap.\n */\nexport function WorkbenchSummaryCard({ open, className, children }: WorkbenchSummaryCardProps) {\n  const reduce = useReducedMotion() ?? false;\n\n  return (\n    <AnimatePresence>\n      {open ? (\n        <motion.div\n          initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.96, x: 8 }}\n          animate={reduce ? { opacity: 1 } : { opacity: 1, scale: 1, x: 0 }}\n          exit={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.96, x: 8 }}\n          transition={reduce ? { duration: 0.15, ease: EASE_OUT } : SPRING_PANEL}\n          style={{\n            transformOrigin: \"top right\",\n            boxShadow:\n              \"0 0 0 0.5px var(--wb-hairline-subtle), 0 3px 7.5px rgba(0,0,0,0.04), 0 0 20px rgba(0,0,0,0.05)\",\n          }}\n          className={cn(\n            \"absolute right-4 top-[54px] z-20 w-[260px] overflow-hidden rounded-3xl bg-[var(--wb-surface-raised)] pt-2.5 backdrop-blur-xl\",\n            className,\n          )}\n        >\n          <div className=\"flex max-h-[420px] flex-col gap-3 overflow-y-auto pb-1.5\">{children}</div>\n        </motion.div>\n      ) : null}\n    </AnimatePresence>\n  );\n}\n\nexport interface WorkbenchSummarySectionProps {\n  title?: ReactNode;\n  /** Rendered at the right end of the title row — e.g. a small icon button. */\n  action?: ReactNode;\n  className?: string;\n  children?: ReactNode;\n}\n\n/**\n * A titled group of rows inside `WorkbenchSummaryCard`. Only lays out the\n * title row and a trailing hairline divider — row styling (hover surface,\n * icon slot, trailing badge) is left to the caller to compose.\n */\nexport function WorkbenchSummarySection({\n  title,\n  action,\n  className,\n  children,\n}: WorkbenchSummarySectionProps) {\n  return (\n    <section\n      className={cn(\n        \"flex flex-col border-[var(--wb-border)] border-b-[0.5px] px-2 pb-3 last:border-0\",\n        className,\n      )}\n    >\n      {title ? (\n        <div className=\"flex items-center justify-between px-1.5 pb-1\">\n          <span className=\"text-muted-foreground text-xs\">{title}</span>\n          {action}\n        </div>\n      ) : null}\n      {children}\n    </section>\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"}]}