{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "type": "registry:ui",
  "slug": "thinking-panel",
  "name": "Thinking Panel",
  "title": "Thinking Panel",
  "description": "An AI activity panel with a live timer, expandable source chips and spring-driven progress steps.",
  "summary": "Present application-supplied activity with a compact thinking indicator, elapsed time, linked sources and a connected step timeline. Source expansion and panel collapse share a damped spring. Stream steps as work progresses, then show a completed or interrupted state. The studio replays a clearly labelled seven-second offsite-planning example inspired by the supplied video.",
  "category": "ai-stuff",
  "tags": [
    "ai",
    "thinking",
    "activity",
    "sources",
    "progress",
    "gsap",
    "spring"
  ],
  "dependencies": [
    "gsap",
    "@gsap/react",
    "motion",
    "lucide-react",
    "clsx",
    "tailwind-merge"
  ],
  "version": "1.0.0",
  "createdDate": "2026-10-23",
  "updatedDate": "2026-10-23",
  "interactive": true,
  "supportsColor": false,
  "highlights": [
    "Live elapsed time or application-controlled timing",
    "Spring-driven expansion and coordinated step entrances",
    "Expandable, wrapping source chips with safe external links",
    "Profile Menu-style shared source highlight with spring-driven hover, keyboard focus and resize alignment",
    "Streaming steps, completion and explicit interruption states",
    "Keyboard disclosure controls and live activity announcements",
    "Responsive light/dark themes and live reduced-motion support",
    "Scoped GSAP cleanup with offscreen and background activity pausing"
  ],
  "anatomy": [
    "Disclosure header with nine-pixel activity indicator and timer",
    "Search sources with expandable chips",
    "Connected activity steps",
    "Completion or interruption result"
  ],
  "physics": {
    "engine": "GSAP + spring easing",
    "description": "A shared damped spring coordinates measured disclosure height, chevron rotation and step movement. Content enters through transform and opacity tweens; ResizeObserver retargets the panel when sources wrap. All observers, tickers and timelines are cleaned up. Source chips share a single highlight that follows pointer and keyboard focus with the same spring response.",
    "parameters": [
      {
        "label": "Stiffness",
        "value": "380 (120–600)"
      },
      {
        "label": "Damping",
        "value": "34 (20–60)"
      },
      {
        "label": "Speed",
        "value": "1× (0.5–2)"
      }
    ]
  },
  "accessibility": {
    "role": "region",
    "aria": "Labelled activity region, native disclosure buttons with expanded state, inert collapsed content, semantic ordered steps, safe named external links, polite step announcements and explicit error alerts. Timer updates are excluded from live announcements.",
    "reducedMotion": "Disclosure and steps settle immediately; activity pixels stay static. Preferences can change while mounted.",
    "keyboard": [
      {
        "key": "Tab / Shift+Tab",
        "description": "Move through the header, source links and source disclosure."
      },
      {
        "key": "Enter / Space",
        "description": "Toggle the panel or source disclosure."
      },
      {
        "key": "Escape",
        "description": "Hide extra sources and return focus to their trigger; otherwise collapse the panel and focus its header."
      }
    ]
  },
  "guidelines": {
    "recommended": [
      "Display search or agent activity alongside an AI response.",
      "Append meaningful activity summaries as your application progresses."
    ],
    "bestPractices": [
      "Supply real sources and steps; the primitive does not perform searches or invent activity.",
      "Use stable unique source and step IDs, and plain-language step labels.",
      "Pass status=complete or status=error when work stops; supply a safe error message.",
      "Remount with a request key to reset the internal timer between independent tasks.",
      "Provide elapsedSeconds for server-authoritative timing and retain expanded state with expanded/onExpandedChange when needed."
    ]
  },
  "props": [
    {
      "name": "sources",
      "type": "readonly ThinkingSource[]",
      "defaultValue": "[]",
      "description": "Stable id, label, optional HTTP(S) href and optional ReactNode icon for each real source."
    },
    {
      "name": "steps",
      "type": "readonly ThinkingStep[]",
      "defaultValue": "[]",
      "description": "Stable id and label for each activity step. Append as work progresses."
    },
    {
      "name": "status",
      "type": "'thinking' | 'complete' | 'error'",
      "defaultValue": "'thinking'",
      "description": "Activity lifecycle. Stops the internal timer when work finishes or fails."
    },
    {
      "name": "elapsedSeconds",
      "type": "number",
      "description": "Optional application-controlled elapsed time; otherwise uses the live internal timer."
    },
    {
      "name": "expanded",
      "type": "boolean",
      "description": "Controlled panel disclosure."
    },
    {
      "name": "defaultExpanded",
      "type": "boolean",
      "defaultValue": "true",
      "description": "Initial uncontrolled panel disclosure."
    },
    {
      "name": "onExpandedChange",
      "type": "(expanded: boolean) => void",
      "description": "Receives disclosure changes."
    },
    {
      "name": "visibleSources",
      "type": "number",
      "defaultValue": "4",
      "description": "Number of source chips visible before the more button, bounded to 1–50."
    },
    {
      "name": "completionLabel",
      "type": "string",
      "defaultValue": "'Finished thinking'",
      "description": "Completion text and announcement."
    },
    {
      "name": "errorMessage",
      "type": "string",
      "defaultValue": "'Something went wrong. Please try again.'",
      "description": "Safe interruption text and alert."
    },
    {
      "name": "width",
      "type": "number",
      "defaultValue": "440",
      "description": "Preferred width in pixels, bounded to 240–960 and constrained by the container."
    },
    {
      "name": "stiffness",
      "type": "number",
      "defaultValue": "380",
      "description": "Spring stiffness, bounded to 120–600."
    },
    {
      "name": "damping",
      "type": "number",
      "defaultValue": "34",
      "description": "Spring damping, bounded to 20–60."
    },
    {
      "name": "speed",
      "type": "number",
      "defaultValue": "1",
      "description": "Motion speed multiplier, bounded to 0.5–2. Does not change real elapsed time."
    },
    {
      "name": "className",
      "type": "string",
      "description": "Additional root classes."
    },
    {
      "name": "ref",
      "type": "Ref<HTMLDivElement>",
      "description": "Root element ref."
    }
  ],
  "usage": "import { ThinkingPanel } from '@/components/ui/thinking-panel';\n\n<ThinkingPanel\n  key={requestId}\n  status={isComplete ? 'complete' : 'thinking'}\n  sources={sources}\n  steps={activitySteps}\n/>",
  "files": [
    {
      "name": "thinking-panel.tsx",
      "path": "src/registry/ui/thinking-panel.tsx",
      "code": "\"use client\";\n\nimport { useCallback, useId, useRef, useState, type Ref } from \"react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { Check, ChevronDown, Globe2, TriangleAlert } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  thinkingPanelBounded,\n  type ThinkingSource,\n  type ThinkingStatus,\n  type ThinkingStep,\n} from \"./thinking-panel-types\";\nimport { useThinkingPanelMotion } from \"./use-thinking-panel-motion\";\nimport { ThinkingPanelSources } from \"./thinking-panel-sources\";\n\nexport type {\n  ThinkingSource,\n  ThinkingStatus,\n  ThinkingStep,\n} from \"./thinking-panel-types\";\n\ngsap.registerPlugin(useGSAP);\n\nexport interface ThinkingPanelProps {\n  sources?: readonly ThinkingSource[];\n  steps?: readonly ThinkingStep[];\n  status?: ThinkingStatus;\n  elapsedSeconds?: number;\n  expanded?: boolean;\n  defaultExpanded?: boolean;\n  onExpandedChange?: (expanded: boolean) => void;\n  visibleSources?: number;\n  completionLabel?: string;\n  errorMessage?: string;\n  width?: number;\n  stiffness?: number;\n  damping?: number;\n  speed?: number;\n  className?: string;\n  ref?: Ref<HTMLDivElement>;\n}\n\nexport function ThinkingPanel({\n  sources = [],\n  steps = [],\n  status = \"thinking\",\n  elapsedSeconds,\n  expanded,\n  defaultExpanded = true,\n  onExpandedChange,\n  visibleSources = 4,\n  completionLabel = \"Finished thinking\",\n  errorMessage = \"Something went wrong. Please try again.\",\n  width = 440,\n  stiffness = 380,\n  damping = 34,\n  speed = 1,\n  className,\n  ref,\n}: ThinkingPanelProps) {\n  const uid = useId();\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 sourcesTriggerRef = useRef<HTMLButtonElement>(null);\n  const clockRef = useRef({ accumulated: 0, started: 0 });\n  const [seconds, setSeconds] = useState(0);\n  const [ownExpanded, setOwnExpanded] = useState(defaultExpanded);\n  const [showAllSources, setShowAllSources] = useState(false);\n  const [initialExpanded] = useState(expanded ?? defaultExpanded);\n  const isOpen = expanded ?? ownExpanded;\n  const sourceLimit = Math.floor(\n    thinkingPanelBounded(visibleSources, 4, 1, 50),\n  );\n  const displayedSources = showAllSources\n    ? sources\n    : sources.slice(0, sourceLimit);\n  const remaining = Math.max(0, sources.length - sourceLimit);\n  const elapsed = Math.floor(\n    thinkingPanelBounded(\n      elapsedSeconds ?? seconds,\n      0,\n      0,\n      Number.MAX_SAFE_INTEGER,\n    ),\n  );\n  const signature = JSON.stringify([\n    displayedSources.map((source) => source.id),\n    steps.map((step) => step.id),\n    status,\n    showAllSources,\n  ]);\n\n  useGSAP(\n    () => {\n      const clock = clockRef.current;\n      if (elapsedSeconds !== undefined) return;\n      if (status !== \"thinking\") {\n        setSeconds(Math.floor(clock.accumulated / 1000));\n        return;\n      }\n      clock.started = performance.now();\n      let lastSecond = -1;\n      const tick = () => {\n        const next = Math.floor(\n          (clock.accumulated + performance.now() - clock.started) / 1000,\n        );\n        if (next !== lastSecond) {\n          lastSecond = next;\n          setSeconds(next);\n        }\n      };\n      gsap.ticker.add(tick);\n      return () => {\n        clock.accumulated += performance.now() - clock.started;\n        gsap.ticker.remove(tick);\n      };\n    },\n    { dependencies: [status, elapsedSeconds], revertOnUpdate: true },\n  );\n\n  useThinkingPanelMotion({\n    rootRef,\n    panelRef,\n    contentRef,\n    signature,\n    expanded: isOpen,\n    status,\n    stiffness,\n    damping,\n    speed,\n  });\n\n  useGSAP(\n    () => {\n      if (!isOpen && contentRef.current?.contains(document.activeElement))\n        triggerRef.current?.focus({ preventScroll: true });\n    },\n    { dependencies: [isOpen] },\n  );\n\n  const attachRef = useCallback(\n    (node: HTMLDivElement | null) => {\n      rootRef.current = node;\n      if (typeof ref === \"function\") {\n        const cleanup = ref(node);\n        return () => {\n          rootRef.current = null;\n          if (typeof cleanup === \"function\") cleanup();\n          else ref(null);\n        };\n      }\n      if (ref) ref.current = node;\n      return () => {\n        rootRef.current = null;\n        if (ref) ref.current = null;\n      };\n    },\n    [ref],\n  );\n\n  const changeExpanded = (next: boolean) => {\n    if (expanded === undefined) setOwnExpanded(next);\n    onExpandedChange?.(next);\n    if (!next) triggerRef.current?.focus({ preventScroll: true });\n  };\n\n  return (\n    <div\n      ref={attachRef}\n      data-slot=\"thinking-panel\"\n      data-expanded={isOpen}\n      data-status={status}\n      role=\"region\"\n      aria-label=\"AI activity\"\n      className={cn(\n        \"relative max-w-full overflow-hidden rounded-2xl border border-zinc-200/80 bg-white text-[13px] leading-5 text-zinc-600 shadow-[0_8px_32px_-20px_#00000020] dark:border-white/[0.08] dark:bg-[#1b1b1d] dark:text-zinc-400 dark:shadow-[0_12px_40px_-20px_#00000050]\",\n        className,\n      )}\n      style={{ width: thinkingPanelBounded(width, 440, 240, 960) }}\n      onKeyDown={(event) => {\n        if (event.key !== \"Escape\" || !isOpen) return;\n        event.preventDefault();\n        event.stopPropagation();\n        if (\n          showAllSources &&\n          event.target instanceof Node &&\n          contentRef.current?.contains(event.target)\n        ) {\n          setShowAllSources(false);\n          sourcesTriggerRef.current?.focus({ preventScroll: true });\n        } else changeExpanded(false);\n      }}\n    >\n      <button\n        ref={triggerRef}\n        type=\"button\"\n        aria-expanded={isOpen}\n        aria-controls={`${uid}-content`}\n        onClick={() => changeExpanded(!isOpen)}\n        className=\"flex min-h-13 w-full cursor-pointer items-center gap-2.5 rounded-2xl px-4 text-left outline-none hover:bg-zinc-50 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-violet-400 dark:hover:bg-white/[0.025]\"\n      >\n        <span\n          aria-hidden=\"true\"\n          className=\"grid size-3.5 shrink-0 grid-cols-3 gap-[2px] text-violet-500 dark:text-violet-400\"\n        >\n          {Array.from({ length: 9 }, (_, index) => (\n            <span\n              key={index}\n              data-thinking-pixel\n              className=\"rounded-[1px] bg-current\"\n              style={{ opacity: 0.65 }}\n            />\n          ))}\n        </span>\n        <span className=\"font-medium text-zinc-700 dark:text-zinc-300\">\n          {status === \"thinking\"\n            ? \"Thinking\"\n            : status === \"error\"\n              ? \"Interrupted after\"\n              : \"Worked for\"}{\" \"}\n          <span\n            aria-hidden=\"true\"\n            className=\"font-normal tabular-nums text-zinc-500 dark:text-zinc-400\"\n          >\n            {elapsed}s\n          </span>\n          <span className=\"sr-only\">\n            {status === \"thinking\" ? \"in progress\" : `${elapsed} seconds`}\n          </span>\n        </span>\n        <ChevronDown\n          data-thinking-chevron\n          aria-hidden=\"true\"\n          className=\"ml-auto size-4 shrink-0 text-zinc-400\"\n          style={{\n            transform: initialExpanded ? \"rotate(0deg)\" : \"rotate(-90deg)\",\n          }}\n        />\n      </button>\n      <div\n        ref={panelRef}\n        id={`${uid}-content`}\n        inert={!isOpen}\n        aria-hidden={!isOpen}\n        className=\"overflow-hidden\"\n        style={{ height: initialExpanded ? \"auto\" : 0 }}\n      >\n        <div\n          ref={contentRef}\n          className=\"border-t border-zinc-100 px-4 pt-4 pb-4 dark:border-white/[0.055]\"\n        >\n          <ol aria-label=\"Activity steps\" className=\"space-y-0\">\n            <li\n              data-thinking-row=\"sources\"\n              className=\"relative grid grid-cols-[14px_minmax(0,1fr)] gap-x-2.5 pb-3\"\n            >\n              <span\n                aria-hidden=\"true\"\n                className=\"absolute top-5 bottom-1 left-[6px] w-px bg-zinc-200 dark:bg-white/[0.09]\"\n              />\n              <Globe2\n                aria-hidden=\"true\"\n                className=\"mt-0.5 size-3.5 text-zinc-400 dark:text-zinc-500\"\n              />\n              <div className=\"min-w-0\">\n                <p className=\"mb-2\">\n                  {sources.length\n                    ? \"Searched\"\n                    : status === \"thinking\"\n                      ? \"Searching sources…\"\n                      : \"No sources searched\"}\n                </p>\n                {sources.length > 0 && (\n                  <ThinkingPanelSources\n                    sources={displayedSources}\n                    remaining={remaining}\n                    showAll={showAllSources}\n                    onToggle={() => setShowAllSources(!showAllSources)}\n                    triggerRef={sourcesTriggerRef}\n                    expanded={isOpen}\n                    stiffness={stiffness}\n                    damping={damping}\n                    speed={speed}\n                  />\n                )}\n              </div>\n            </li>\n            {steps.map((step, index) => (\n              <li\n                key={step.id}\n                data-thinking-row={`step-${step.id}`}\n                className=\"relative grid grid-cols-[14px_minmax(0,1fr)] items-start gap-x-2.5 pb-3\"\n              >\n                <span\n                  aria-hidden=\"true\"\n                  className=\"absolute top-4 bottom-0 left-[6px] w-px bg-zinc-200 dark:bg-white/[0.09]\"\n                />\n                <span\n                  aria-hidden=\"true\"\n                  data-thinking-active={\n                    status === \"thinking\" && index === steps.length - 1\n                      ? \"true\"\n                      : undefined\n                  }\n                  className={cn(\n                    \"mx-auto mt-2 size-1.5 rounded-full\",\n                    status === \"thinking\" && index === steps.length - 1\n                      ? \"bg-violet-500 dark:bg-violet-400\"\n                      : \"bg-zinc-300 dark:bg-zinc-600\",\n                  )}\n                />\n                <span className=\"min-w-0 break-words\">{step.label}</span>\n              </li>\n            ))}\n            {status !== \"thinking\" && (\n              <li\n                data-thinking-row=\"result\"\n                className=\"grid grid-cols-[14px_minmax(0,1fr)] items-start gap-x-2.5\"\n              >\n                {status === \"complete\" ? (\n                  <span\n                    aria-hidden=\"true\"\n                    className=\"mt-0.5 flex size-3.5 items-center justify-center rounded-full bg-violet-500 dark:bg-violet-400\"\n                  >\n                    <Check\n                      className=\"size-2.5 text-white dark:text-zinc-950\"\n                      strokeWidth={3}\n                    />\n                  </span>\n                ) : (\n                  <TriangleAlert\n                    aria-hidden=\"true\"\n                    className=\"mt-0.5 size-3.5 text-amber-600 dark:text-amber-400\"\n                  />\n                )}\n                <span className=\"font-medium text-zinc-700 dark:text-zinc-300\">\n                  {status === \"complete\" ? completionLabel : errorMessage}\n                </span>\n              </li>\n            )}\n          </ol>\n        </div>\n      </div>\n      <span\n        role={status === \"error\" ? \"alert\" : \"status\"}\n        aria-atomic=\"true\"\n        className=\"sr-only\"\n      >\n        {status === \"complete\"\n          ? completionLabel\n          : status === \"error\"\n            ? errorMessage\n            : (steps.at(-1)?.label ?? \"Searching sources\")}\n      </span>\n    </div>\n  );\n}\n",
      "content": "\"use client\";\n\nimport { useCallback, useId, useRef, useState, type Ref } from \"react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { Check, ChevronDown, Globe2, TriangleAlert } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  thinkingPanelBounded,\n  type ThinkingSource,\n  type ThinkingStatus,\n  type ThinkingStep,\n} from \"./thinking-panel-types\";\nimport { useThinkingPanelMotion } from \"./use-thinking-panel-motion\";\nimport { ThinkingPanelSources } from \"./thinking-panel-sources\";\n\nexport type {\n  ThinkingSource,\n  ThinkingStatus,\n  ThinkingStep,\n} from \"./thinking-panel-types\";\n\ngsap.registerPlugin(useGSAP);\n\nexport interface ThinkingPanelProps {\n  sources?: readonly ThinkingSource[];\n  steps?: readonly ThinkingStep[];\n  status?: ThinkingStatus;\n  elapsedSeconds?: number;\n  expanded?: boolean;\n  defaultExpanded?: boolean;\n  onExpandedChange?: (expanded: boolean) => void;\n  visibleSources?: number;\n  completionLabel?: string;\n  errorMessage?: string;\n  width?: number;\n  stiffness?: number;\n  damping?: number;\n  speed?: number;\n  className?: string;\n  ref?: Ref<HTMLDivElement>;\n}\n\nexport function ThinkingPanel({\n  sources = [],\n  steps = [],\n  status = \"thinking\",\n  elapsedSeconds,\n  expanded,\n  defaultExpanded = true,\n  onExpandedChange,\n  visibleSources = 4,\n  completionLabel = \"Finished thinking\",\n  errorMessage = \"Something went wrong. Please try again.\",\n  width = 440,\n  stiffness = 380,\n  damping = 34,\n  speed = 1,\n  className,\n  ref,\n}: ThinkingPanelProps) {\n  const uid = useId();\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 sourcesTriggerRef = useRef<HTMLButtonElement>(null);\n  const clockRef = useRef({ accumulated: 0, started: 0 });\n  const [seconds, setSeconds] = useState(0);\n  const [ownExpanded, setOwnExpanded] = useState(defaultExpanded);\n  const [showAllSources, setShowAllSources] = useState(false);\n  const [initialExpanded] = useState(expanded ?? defaultExpanded);\n  const isOpen = expanded ?? ownExpanded;\n  const sourceLimit = Math.floor(\n    thinkingPanelBounded(visibleSources, 4, 1, 50),\n  );\n  const displayedSources = showAllSources\n    ? sources\n    : sources.slice(0, sourceLimit);\n  const remaining = Math.max(0, sources.length - sourceLimit);\n  const elapsed = Math.floor(\n    thinkingPanelBounded(\n      elapsedSeconds ?? seconds,\n      0,\n      0,\n      Number.MAX_SAFE_INTEGER,\n    ),\n  );\n  const signature = JSON.stringify([\n    displayedSources.map((source) => source.id),\n    steps.map((step) => step.id),\n    status,\n    showAllSources,\n  ]);\n\n  useGSAP(\n    () => {\n      const clock = clockRef.current;\n      if (elapsedSeconds !== undefined) return;\n      if (status !== \"thinking\") {\n        setSeconds(Math.floor(clock.accumulated / 1000));\n        return;\n      }\n      clock.started = performance.now();\n      let lastSecond = -1;\n      const tick = () => {\n        const next = Math.floor(\n          (clock.accumulated + performance.now() - clock.started) / 1000,\n        );\n        if (next !== lastSecond) {\n          lastSecond = next;\n          setSeconds(next);\n        }\n      };\n      gsap.ticker.add(tick);\n      return () => {\n        clock.accumulated += performance.now() - clock.started;\n        gsap.ticker.remove(tick);\n      };\n    },\n    { dependencies: [status, elapsedSeconds], revertOnUpdate: true },\n  );\n\n  useThinkingPanelMotion({\n    rootRef,\n    panelRef,\n    contentRef,\n    signature,\n    expanded: isOpen,\n    status,\n    stiffness,\n    damping,\n    speed,\n  });\n\n  useGSAP(\n    () => {\n      if (!isOpen && contentRef.current?.contains(document.activeElement))\n        triggerRef.current?.focus({ preventScroll: true });\n    },\n    { dependencies: [isOpen] },\n  );\n\n  const attachRef = useCallback(\n    (node: HTMLDivElement | null) => {\n      rootRef.current = node;\n      if (typeof ref === \"function\") {\n        const cleanup = ref(node);\n        return () => {\n          rootRef.current = null;\n          if (typeof cleanup === \"function\") cleanup();\n          else ref(null);\n        };\n      }\n      if (ref) ref.current = node;\n      return () => {\n        rootRef.current = null;\n        if (ref) ref.current = null;\n      };\n    },\n    [ref],\n  );\n\n  const changeExpanded = (next: boolean) => {\n    if (expanded === undefined) setOwnExpanded(next);\n    onExpandedChange?.(next);\n    if (!next) triggerRef.current?.focus({ preventScroll: true });\n  };\n\n  return (\n    <div\n      ref={attachRef}\n      data-slot=\"thinking-panel\"\n      data-expanded={isOpen}\n      data-status={status}\n      role=\"region\"\n      aria-label=\"AI activity\"\n      className={cn(\n        \"relative max-w-full overflow-hidden rounded-2xl border border-zinc-200/80 bg-white text-[13px] leading-5 text-zinc-600 shadow-[0_8px_32px_-20px_#00000020] dark:border-white/[0.08] dark:bg-[#1b1b1d] dark:text-zinc-400 dark:shadow-[0_12px_40px_-20px_#00000050]\",\n        className,\n      )}\n      style={{ width: thinkingPanelBounded(width, 440, 240, 960) }}\n      onKeyDown={(event) => {\n        if (event.key !== \"Escape\" || !isOpen) return;\n        event.preventDefault();\n        event.stopPropagation();\n        if (\n          showAllSources &&\n          event.target instanceof Node &&\n          contentRef.current?.contains(event.target)\n        ) {\n          setShowAllSources(false);\n          sourcesTriggerRef.current?.focus({ preventScroll: true });\n        } else changeExpanded(false);\n      }}\n    >\n      <button\n        ref={triggerRef}\n        type=\"button\"\n        aria-expanded={isOpen}\n        aria-controls={`${uid}-content`}\n        onClick={() => changeExpanded(!isOpen)}\n        className=\"flex min-h-13 w-full cursor-pointer items-center gap-2.5 rounded-2xl px-4 text-left outline-none hover:bg-zinc-50 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-violet-400 dark:hover:bg-white/[0.025]\"\n      >\n        <span\n          aria-hidden=\"true\"\n          className=\"grid size-3.5 shrink-0 grid-cols-3 gap-[2px] text-violet-500 dark:text-violet-400\"\n        >\n          {Array.from({ length: 9 }, (_, index) => (\n            <span\n              key={index}\n              data-thinking-pixel\n              className=\"rounded-[1px] bg-current\"\n              style={{ opacity: 0.65 }}\n            />\n          ))}\n        </span>\n        <span className=\"font-medium text-zinc-700 dark:text-zinc-300\">\n          {status === \"thinking\"\n            ? \"Thinking\"\n            : status === \"error\"\n              ? \"Interrupted after\"\n              : \"Worked for\"}{\" \"}\n          <span\n            aria-hidden=\"true\"\n            className=\"font-normal tabular-nums text-zinc-500 dark:text-zinc-400\"\n          >\n            {elapsed}s\n          </span>\n          <span className=\"sr-only\">\n            {status === \"thinking\" ? \"in progress\" : `${elapsed} seconds`}\n          </span>\n        </span>\n        <ChevronDown\n          data-thinking-chevron\n          aria-hidden=\"true\"\n          className=\"ml-auto size-4 shrink-0 text-zinc-400\"\n          style={{\n            transform: initialExpanded ? \"rotate(0deg)\" : \"rotate(-90deg)\",\n          }}\n        />\n      </button>\n      <div\n        ref={panelRef}\n        id={`${uid}-content`}\n        inert={!isOpen}\n        aria-hidden={!isOpen}\n        className=\"overflow-hidden\"\n        style={{ height: initialExpanded ? \"auto\" : 0 }}\n      >\n        <div\n          ref={contentRef}\n          className=\"border-t border-zinc-100 px-4 pt-4 pb-4 dark:border-white/[0.055]\"\n        >\n          <ol aria-label=\"Activity steps\" className=\"space-y-0\">\n            <li\n              data-thinking-row=\"sources\"\n              className=\"relative grid grid-cols-[14px_minmax(0,1fr)] gap-x-2.5 pb-3\"\n            >\n              <span\n                aria-hidden=\"true\"\n                className=\"absolute top-5 bottom-1 left-[6px] w-px bg-zinc-200 dark:bg-white/[0.09]\"\n              />\n              <Globe2\n                aria-hidden=\"true\"\n                className=\"mt-0.5 size-3.5 text-zinc-400 dark:text-zinc-500\"\n              />\n              <div className=\"min-w-0\">\n                <p className=\"mb-2\">\n                  {sources.length\n                    ? \"Searched\"\n                    : status === \"thinking\"\n                      ? \"Searching sources…\"\n                      : \"No sources searched\"}\n                </p>\n                {sources.length > 0 && (\n                  <ThinkingPanelSources\n                    sources={displayedSources}\n                    remaining={remaining}\n                    showAll={showAllSources}\n                    onToggle={() => setShowAllSources(!showAllSources)}\n                    triggerRef={sourcesTriggerRef}\n                    expanded={isOpen}\n                    stiffness={stiffness}\n                    damping={damping}\n                    speed={speed}\n                  />\n                )}\n              </div>\n            </li>\n            {steps.map((step, index) => (\n              <li\n                key={step.id}\n                data-thinking-row={`step-${step.id}`}\n                className=\"relative grid grid-cols-[14px_minmax(0,1fr)] items-start gap-x-2.5 pb-3\"\n              >\n                <span\n                  aria-hidden=\"true\"\n                  className=\"absolute top-4 bottom-0 left-[6px] w-px bg-zinc-200 dark:bg-white/[0.09]\"\n                />\n                <span\n                  aria-hidden=\"true\"\n                  data-thinking-active={\n                    status === \"thinking\" && index === steps.length - 1\n                      ? \"true\"\n                      : undefined\n                  }\n                  className={cn(\n                    \"mx-auto mt-2 size-1.5 rounded-full\",\n                    status === \"thinking\" && index === steps.length - 1\n                      ? \"bg-violet-500 dark:bg-violet-400\"\n                      : \"bg-zinc-300 dark:bg-zinc-600\",\n                  )}\n                />\n                <span className=\"min-w-0 break-words\">{step.label}</span>\n              </li>\n            ))}\n            {status !== \"thinking\" && (\n              <li\n                data-thinking-row=\"result\"\n                className=\"grid grid-cols-[14px_minmax(0,1fr)] items-start gap-x-2.5\"\n              >\n                {status === \"complete\" ? (\n                  <span\n                    aria-hidden=\"true\"\n                    className=\"mt-0.5 flex size-3.5 items-center justify-center rounded-full bg-violet-500 dark:bg-violet-400\"\n                  >\n                    <Check\n                      className=\"size-2.5 text-white dark:text-zinc-950\"\n                      strokeWidth={3}\n                    />\n                  </span>\n                ) : (\n                  <TriangleAlert\n                    aria-hidden=\"true\"\n                    className=\"mt-0.5 size-3.5 text-amber-600 dark:text-amber-400\"\n                  />\n                )}\n                <span className=\"font-medium text-zinc-700 dark:text-zinc-300\">\n                  {status === \"complete\" ? completionLabel : errorMessage}\n                </span>\n              </li>\n            )}\n          </ol>\n        </div>\n      </div>\n      <span\n        role={status === \"error\" ? \"alert\" : \"status\"}\n        aria-atomic=\"true\"\n        className=\"sr-only\"\n      >\n        {status === \"complete\"\n          ? completionLabel\n          : status === \"error\"\n            ? errorMessage\n            : (steps.at(-1)?.label ?? \"Searching sources\")}\n      </span>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/thinking-panel.tsx"
    },
    {
      "name": "thinking-panel-types.ts",
      "path": "src/registry/ui/thinking-panel-types.ts",
      "code": "import type { ReactNode } from \"react\";\n\nexport interface ThinkingSource {\n  id: string;\n  label: string;\n  href?: string;\n  icon?: ReactNode;\n}\n\nexport interface ThinkingStep {\n  id: string;\n  label: string;\n}\n\nexport type ThinkingStatus = \"thinking\" | \"complete\" | \"error\";\n\nexport const thinkingPanelBounded = (\n  value: number,\n  fallback: number,\n  min: number,\n  max: number,\n) => (Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback);\n\nexport function thinkingSourceUrl(href?: string) {\n  if (!href) return undefined;\n  try {\n    const url = new URL(href);\n    return url.protocol === \"https:\" || url.protocol === \"http:\"\n      ? url.href\n      : undefined;\n  } catch {\n    return undefined;\n  }\n}\n",
      "content": "import type { ReactNode } from \"react\";\n\nexport interface ThinkingSource {\n  id: string;\n  label: string;\n  href?: string;\n  icon?: ReactNode;\n}\n\nexport interface ThinkingStep {\n  id: string;\n  label: string;\n}\n\nexport type ThinkingStatus = \"thinking\" | \"complete\" | \"error\";\n\nexport const thinkingPanelBounded = (\n  value: number,\n  fallback: number,\n  min: number,\n  max: number,\n) => (Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback);\n\nexport function thinkingSourceUrl(href?: string) {\n  if (!href) return undefined;\n  try {\n    const url = new URL(href);\n    return url.protocol === \"https:\" || url.protocol === \"http:\"\n      ? url.href\n      : undefined;\n  } catch {\n    return undefined;\n  }\n}\n",
      "type": "registry:ui",
      "target": "@ui/thinking-panel-types.ts"
    },
    {
      "name": "use-thinking-panel-motion.ts",
      "path": "src/registry/ui/use-thinking-panel-motion.ts",
      "code": "\"use client\";\n\nimport { useRef, type RefObject } from \"react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { type ThinkingStatus } from \"./thinking-panel-types\";\nimport { thinkingPanelSpring } from \"./thinking-panel-spring\";\n\ngsap.registerPlugin(useGSAP);\n\nexport function useThinkingPanelMotion({\n  rootRef,\n  panelRef,\n  contentRef,\n  signature,\n  expanded,\n  status,\n  stiffness,\n  damping,\n  speed,\n}: {\n  rootRef: RefObject<HTMLDivElement | null>;\n  panelRef: RefObject<HTMLDivElement | null>;\n  contentRef: RefObject<HTMLDivElement | null>;\n  signature: string;\n  expanded: boolean;\n  status: ThinkingStatus;\n  stiffness: number;\n  damping: number;\n  speed: number;\n}) {\n  const syncRef = useRef<(() => 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) return;\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          const reduced = Boolean(context.conditions?.reduced);\n          const { duration, ease } = thinkingPanelSpring(\n            stiffness,\n            damping,\n            speed,\n            reduced,\n          );\n          let timeline: gsap.core.Timeline | null = null;\n          let positions = new Map<string, { top: number; left: number }>();\n          let visible = true;\n          const pulse = reduced\n            ? null\n            : gsap.to(\"[data-thinking-pixel]\", {\n                opacity: 0.2,\n                duration: 0.6,\n                stagger: { each: 0.08, repeat: -1, yoyo: true },\n                ease: \"sine.inOut\",\n                paused: true,\n              });\n          const updatePulse = () => {\n            if (\n              visible &&\n              !document.hidden &&\n              root.dataset.expanded === \"true\" &&\n              root.dataset.status === \"thinking\"\n            )\n              pulse?.resume();\n            else pulse?.pause();\n          };\n          const sync = context.add(\"sync\", (immediate = false) => {\n            const open = root.dataset.expanded === \"true\";\n            const rows = Array.from(\n              content.querySelectorAll<HTMLElement>(\n                \"[data-thinking-row], [data-thinking-source]\",\n              ),\n            );\n            const measurements = rows.map((row) => ({\n              row,\n              id: row.dataset.thinkingRow\n                ? `row-${row.dataset.thinkingRow}`\n                : `source-${row.dataset.thinkingSource}`,\n              top: row.offsetTop,\n              left: row.offsetLeft,\n              x: Number(gsap.getProperty(row, \"x\")) || 0,\n              y: Number(gsap.getProperty(row, \"y\")) || 0,\n            }));\n            const height = content.offsetHeight;\n            timeline?.kill();\n            timeline = gsap.timeline({\n              defaults: {\n                duration: immediate ? 0 : duration,\n                ease,\n                overwrite: \"auto\",\n              },\n            });\n            timeline.to(panel, { height: open ? height : 0 }, 0);\n            timeline.to(\n              \"[data-thinking-chevron]\",\n              { rotation: open ? 0 : -90 },\n              0,\n            );\n            timeline.to(\n              content,\n              {\n                opacity: open ? 1 : 0,\n                duration: immediate || reduced ? 0 : 0.18,\n                ease: \"power2.out\",\n              },\n              0,\n            );\n            for (const { row, id, top, left, x, y } of measurements) {\n              const previous = positions.get(id);\n              if (previous !== undefined)\n                timeline.fromTo(\n                  row,\n                  { x: previous.left - left + x, y: previous.top - top + y },\n                  { x: 0, y: 0, opacity: 1 },\n                  0,\n                );\n              else if (!immediate && open)\n                timeline.fromTo(\n                  row,\n                  { y: reduced ? 0 : 7, opacity: 0 },\n                  {\n                    y: 0,\n                    opacity: 1,\n                    duration: reduced ? 0 : Math.min(duration, 0.4),\n                  },\n                  0,\n                );\n            }\n            positions = new Map(\n              measurements.map(({ id, top, left }) => [id, { top, left }]),\n            );\n            updatePulse();\n          });\n          syncRef.current = () => sync();\n          sync(true);\n          const observer = new ResizeObserver(() => sync());\n          observer.observe(content);\n          const visibility = new IntersectionObserver(([entry]) => {\n            visible = entry.isIntersecting;\n            updatePulse();\n          });\n          visibility.observe(root);\n          document.addEventListener(\"visibilitychange\", updatePulse);\n          return () => {\n            observer.disconnect();\n            visibility.disconnect();\n            document.removeEventListener(\"visibilitychange\", updatePulse);\n            timeline?.kill();\n            pulse?.kill();\n            syncRef.current = null;\n          };\n        },\n        root,\n      );\n      return () => media.revert();\n    },\n    {\n      scope: rootRef,\n      dependencies: [stiffness, damping, speed],\n      revertOnUpdate: true,\n    },\n  );\n\n  useGSAP(\n    (context) => {\n      context.ignore(() => syncRef.current?.());\n    },\n    { scope: rootRef, dependencies: [signature, expanded, status] },\n  );\n}\n",
      "content": "\"use client\";\n\nimport { useRef, type RefObject } from \"react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { type ThinkingStatus } from \"./thinking-panel-types\";\nimport { thinkingPanelSpring } from \"./thinking-panel-spring\";\n\ngsap.registerPlugin(useGSAP);\n\nexport function useThinkingPanelMotion({\n  rootRef,\n  panelRef,\n  contentRef,\n  signature,\n  expanded,\n  status,\n  stiffness,\n  damping,\n  speed,\n}: {\n  rootRef: RefObject<HTMLDivElement | null>;\n  panelRef: RefObject<HTMLDivElement | null>;\n  contentRef: RefObject<HTMLDivElement | null>;\n  signature: string;\n  expanded: boolean;\n  status: ThinkingStatus;\n  stiffness: number;\n  damping: number;\n  speed: number;\n}) {\n  const syncRef = useRef<(() => 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) return;\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          const reduced = Boolean(context.conditions?.reduced);\n          const { duration, ease } = thinkingPanelSpring(\n            stiffness,\n            damping,\n            speed,\n            reduced,\n          );\n          let timeline: gsap.core.Timeline | null = null;\n          let positions = new Map<string, { top: number; left: number }>();\n          let visible = true;\n          const pulse = reduced\n            ? null\n            : gsap.to(\"[data-thinking-pixel]\", {\n                opacity: 0.2,\n                duration: 0.6,\n                stagger: { each: 0.08, repeat: -1, yoyo: true },\n                ease: \"sine.inOut\",\n                paused: true,\n              });\n          const updatePulse = () => {\n            if (\n              visible &&\n              !document.hidden &&\n              root.dataset.expanded === \"true\" &&\n              root.dataset.status === \"thinking\"\n            )\n              pulse?.resume();\n            else pulse?.pause();\n          };\n          const sync = context.add(\"sync\", (immediate = false) => {\n            const open = root.dataset.expanded === \"true\";\n            const rows = Array.from(\n              content.querySelectorAll<HTMLElement>(\n                \"[data-thinking-row], [data-thinking-source]\",\n              ),\n            );\n            const measurements = rows.map((row) => ({\n              row,\n              id: row.dataset.thinkingRow\n                ? `row-${row.dataset.thinkingRow}`\n                : `source-${row.dataset.thinkingSource}`,\n              top: row.offsetTop,\n              left: row.offsetLeft,\n              x: Number(gsap.getProperty(row, \"x\")) || 0,\n              y: Number(gsap.getProperty(row, \"y\")) || 0,\n            }));\n            const height = content.offsetHeight;\n            timeline?.kill();\n            timeline = gsap.timeline({\n              defaults: {\n                duration: immediate ? 0 : duration,\n                ease,\n                overwrite: \"auto\",\n              },\n            });\n            timeline.to(panel, { height: open ? height : 0 }, 0);\n            timeline.to(\n              \"[data-thinking-chevron]\",\n              { rotation: open ? 0 : -90 },\n              0,\n            );\n            timeline.to(\n              content,\n              {\n                opacity: open ? 1 : 0,\n                duration: immediate || reduced ? 0 : 0.18,\n                ease: \"power2.out\",\n              },\n              0,\n            );\n            for (const { row, id, top, left, x, y } of measurements) {\n              const previous = positions.get(id);\n              if (previous !== undefined)\n                timeline.fromTo(\n                  row,\n                  { x: previous.left - left + x, y: previous.top - top + y },\n                  { x: 0, y: 0, opacity: 1 },\n                  0,\n                );\n              else if (!immediate && open)\n                timeline.fromTo(\n                  row,\n                  { y: reduced ? 0 : 7, opacity: 0 },\n                  {\n                    y: 0,\n                    opacity: 1,\n                    duration: reduced ? 0 : Math.min(duration, 0.4),\n                  },\n                  0,\n                );\n            }\n            positions = new Map(\n              measurements.map(({ id, top, left }) => [id, { top, left }]),\n            );\n            updatePulse();\n          });\n          syncRef.current = () => sync();\n          sync(true);\n          const observer = new ResizeObserver(() => sync());\n          observer.observe(content);\n          const visibility = new IntersectionObserver(([entry]) => {\n            visible = entry.isIntersecting;\n            updatePulse();\n          });\n          visibility.observe(root);\n          document.addEventListener(\"visibilitychange\", updatePulse);\n          return () => {\n            observer.disconnect();\n            visibility.disconnect();\n            document.removeEventListener(\"visibilitychange\", updatePulse);\n            timeline?.kill();\n            pulse?.kill();\n            syncRef.current = null;\n          };\n        },\n        root,\n      );\n      return () => media.revert();\n    },\n    {\n      scope: rootRef,\n      dependencies: [stiffness, damping, speed],\n      revertOnUpdate: true,\n    },\n  );\n\n  useGSAP(\n    (context) => {\n      context.ignore(() => syncRef.current?.());\n    },\n    { scope: rootRef, dependencies: [signature, expanded, status] },\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/use-thinking-panel-motion.ts"
    },
    {
      "name": "thinking-panel-sources.tsx",
      "path": "src/registry/ui/thinking-panel-sources.tsx",
      "code": "\"use client\";\n\nimport { useRef, type RefObject } from \"react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { Globe2 } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { thinkingSourceUrl, type ThinkingSource } from \"./thinking-panel-types\";\nimport { thinkingPanelSpring } from \"./thinking-panel-spring\";\n\ngsap.registerPlugin(useGSAP);\n\nexport function ThinkingPanelSources({\n  sources,\n  remaining,\n  showAll,\n  onToggle,\n  triggerRef,\n  expanded,\n  stiffness,\n  damping,\n  speed,\n}: {\n  sources: readonly ThinkingSource[];\n  remaining: number;\n  showAll: boolean;\n  onToggle: () => void;\n  triggerRef: RefObject<HTMLButtonElement | null>;\n  expanded: boolean;\n  stiffness: number;\n  damping: number;\n  speed: number;\n}) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const highlightRef = useRef<HTMLSpanElement>(null);\n\n  useGSAP(\n    () => {\n      const root = rootRef.current;\n      const highlight = highlightRef.current;\n      if (!root || !highlight) return;\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          const reduced = Boolean(context.conditions?.reduced);\n          const physics = thinkingPanelSpring(\n            stiffness,\n            damping,\n            speed,\n            reduced,\n          );\n          let hovered = window.matchMedia(\"(hover: hover)\").matches\n            ? root.querySelector<HTMLElement>(\"[data-source-target]:hover\")\n            : null;\n          let current: HTMLElement | null = null;\n          let tween: gsap.core.Tween | null = null;\n          const targetFor = (target: EventTarget | null) => {\n            const item =\n              target instanceof Element\n                ? target.closest<HTMLElement>(\"[data-source-target]\")\n                : null;\n            return item && root.contains(item) ? item : null;\n          };\n          const move = context.add(\"move\", (force = false) => {\n            if (hovered && !root.contains(hovered)) hovered = null;\n            const target = expanded\n              ? (hovered ?? targetFor(document.activeElement))\n              : null;\n            if (!force && target === current) return;\n            tween?.kill();\n            if (!target) {\n              current = null;\n              tween = gsap.to(highlight, {\n                autoAlpha: 0,\n                duration: reduced ? 0 : 0.16,\n                ease: \"power2.out\",\n                overwrite: true,\n              });\n              return;\n            }\n            const geometry = {\n              x: target.offsetLeft,\n              y: target.offsetTop,\n              width: target.offsetWidth,\n              height: target.offsetHeight,\n            };\n            if (!current || reduced)\n              gsap.set(highlight, { ...geometry, scale: reduced ? 1 : 0.98 });\n            current = target;\n            tween = gsap.to(highlight, {\n              ...geometry,\n              scale: 1,\n              autoAlpha: 1,\n              ...physics,\n              overwrite: true,\n            });\n          });\n          const enter = (event: PointerEvent) => {\n            if (event.pointerType === \"touch\") return;\n            hovered = targetFor(event.target);\n            move();\n          };\n          const leave = () => {\n            hovered = null;\n            move();\n          };\n          const focus = () => {\n            hovered = null;\n            move(true);\n          };\n          const blurInContext = context.add(\"blur\", (event: FocusEvent) => {\n            if (\n              !root.contains(\n                event.relatedTarget instanceof Node\n                  ? event.relatedTarget\n                  : null,\n              )\n            ) {\n              hovered = null;\n              current = null;\n              tween?.kill();\n              tween = gsap.to(highlight, {\n                autoAlpha: 0,\n                duration: reduced ? 0 : 0.16,\n                overwrite: true,\n              });\n            } else move(true);\n          });\n          const blur = (event: FocusEvent) => blurInContext(event);\n          const observer = new ResizeObserver(() => move(true));\n          observer.observe(root);\n          root\n            .querySelectorAll<HTMLElement>(\"[data-source-target]\")\n            .forEach((target) => observer.observe(target));\n          root.addEventListener(\"pointerover\", enter);\n          root.addEventListener(\"pointerleave\", leave);\n          root.addEventListener(\"focusin\", focus);\n          root.addEventListener(\"focusout\", blur);\n          move(true);\n          return () => {\n            observer.disconnect();\n            root.removeEventListener(\"pointerover\", enter);\n            root.removeEventListener(\"pointerleave\", leave);\n            root.removeEventListener(\"focusin\", focus);\n            root.removeEventListener(\"focusout\", blur);\n            tween?.kill();\n          };\n        },\n        root,\n      );\n      return () => media.revert();\n    },\n    {\n      scope: rootRef,\n      dependencies: [\n        stiffness,\n        damping,\n        speed,\n        expanded,\n        JSON.stringify(sources.map(({ id, href }) => [id, href])),\n      ],\n      revertOnUpdate: true,\n    },\n  );\n\n  const chipClass =\n    \"relative z-10 inline-flex min-h-7 max-w-full items-center gap-1.5 rounded-lg border border-zinc-200/80 bg-zinc-50/60 px-2 py-1 text-[11px] leading-4 text-zinc-600 pointer-coarse:min-h-10 dark:border-white/[0.07] dark:bg-white/[0.025] dark:text-zinc-300\";\n  return (\n    <div\n      ref={rootRef}\n      data-thinking-sources\n      className=\"relative isolate flex flex-wrap items-center gap-1.5\"\n    >\n      <span\n        ref={highlightRef}\n        data-source-highlight\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute top-0 left-0 z-0 rounded-lg bg-zinc-200/80 opacity-0 shadow-[0_2px_8px_-4px_#00000025] dark:bg-white/[0.09]\"\n        style={{ visibility: \"hidden\", transformOrigin: \"center\" }}\n      />\n      {sources.map((source) => {\n        const href = thinkingSourceUrl(source.href);\n        const content = (\n          <>\n            <span\n              data-source-logo\n              aria-hidden=\"true\"\n              className=\"flex size-4 shrink-0 items-center justify-center overflow-hidden rounded-[3px] [&>svg]:size-full\"\n            >\n              {source.icon ?? <Globe2 className=\"size-3.5\" />}\n            </span>\n            <span className=\"truncate\">{source.label}</span>\n          </>\n        );\n        return href ? (\n          <a\n            key={source.id}\n            data-thinking-source={source.id}\n            data-source-target\n            href={href}\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            title={href}\n            className={cn(\n              chipClass,\n              \"outline-none focus-visible:ring-2 focus-visible:ring-violet-400\",\n            )}\n          >\n            {content}\n            <span className=\"sr-only\"> (opens in a new tab)</span>\n          </a>\n        ) : (\n          <span\n            key={source.id}\n            data-thinking-source={source.id}\n            data-source-target\n            className={chipClass}\n          >\n            {content}\n          </span>\n        );\n      })}\n      {remaining > 0 && (\n        <button\n          ref={triggerRef}\n          type=\"button\"\n          data-source-target\n          aria-expanded={showAll}\n          aria-label={\n            showAll ? \"Show fewer sources\" : `Show ${remaining} more sources`\n          }\n          onClick={onToggle}\n          className=\"relative z-10 min-h-7 cursor-pointer rounded-lg px-2 py-1 text-[11px] leading-4 text-zinc-500 outline-none focus-visible:ring-2 focus-visible:ring-violet-400 pointer-coarse:min-h-10 dark:text-zinc-400\"\n        >\n          {showAll ? \"Show less\" : `+${remaining} more`}\n        </button>\n      )}\n    </div>\n  );\n}\n",
      "content": "\"use client\";\n\nimport { useRef, type RefObject } from \"react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { Globe2 } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { thinkingSourceUrl, type ThinkingSource } from \"./thinking-panel-types\";\nimport { thinkingPanelSpring } from \"./thinking-panel-spring\";\n\ngsap.registerPlugin(useGSAP);\n\nexport function ThinkingPanelSources({\n  sources,\n  remaining,\n  showAll,\n  onToggle,\n  triggerRef,\n  expanded,\n  stiffness,\n  damping,\n  speed,\n}: {\n  sources: readonly ThinkingSource[];\n  remaining: number;\n  showAll: boolean;\n  onToggle: () => void;\n  triggerRef: RefObject<HTMLButtonElement | null>;\n  expanded: boolean;\n  stiffness: number;\n  damping: number;\n  speed: number;\n}) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const highlightRef = useRef<HTMLSpanElement>(null);\n\n  useGSAP(\n    () => {\n      const root = rootRef.current;\n      const highlight = highlightRef.current;\n      if (!root || !highlight) return;\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          const reduced = Boolean(context.conditions?.reduced);\n          const physics = thinkingPanelSpring(\n            stiffness,\n            damping,\n            speed,\n            reduced,\n          );\n          let hovered = window.matchMedia(\"(hover: hover)\").matches\n            ? root.querySelector<HTMLElement>(\"[data-source-target]:hover\")\n            : null;\n          let current: HTMLElement | null = null;\n          let tween: gsap.core.Tween | null = null;\n          const targetFor = (target: EventTarget | null) => {\n            const item =\n              target instanceof Element\n                ? target.closest<HTMLElement>(\"[data-source-target]\")\n                : null;\n            return item && root.contains(item) ? item : null;\n          };\n          const move = context.add(\"move\", (force = false) => {\n            if (hovered && !root.contains(hovered)) hovered = null;\n            const target = expanded\n              ? (hovered ?? targetFor(document.activeElement))\n              : null;\n            if (!force && target === current) return;\n            tween?.kill();\n            if (!target) {\n              current = null;\n              tween = gsap.to(highlight, {\n                autoAlpha: 0,\n                duration: reduced ? 0 : 0.16,\n                ease: \"power2.out\",\n                overwrite: true,\n              });\n              return;\n            }\n            const geometry = {\n              x: target.offsetLeft,\n              y: target.offsetTop,\n              width: target.offsetWidth,\n              height: target.offsetHeight,\n            };\n            if (!current || reduced)\n              gsap.set(highlight, { ...geometry, scale: reduced ? 1 : 0.98 });\n            current = target;\n            tween = gsap.to(highlight, {\n              ...geometry,\n              scale: 1,\n              autoAlpha: 1,\n              ...physics,\n              overwrite: true,\n            });\n          });\n          const enter = (event: PointerEvent) => {\n            if (event.pointerType === \"touch\") return;\n            hovered = targetFor(event.target);\n            move();\n          };\n          const leave = () => {\n            hovered = null;\n            move();\n          };\n          const focus = () => {\n            hovered = null;\n            move(true);\n          };\n          const blurInContext = context.add(\"blur\", (event: FocusEvent) => {\n            if (\n              !root.contains(\n                event.relatedTarget instanceof Node\n                  ? event.relatedTarget\n                  : null,\n              )\n            ) {\n              hovered = null;\n              current = null;\n              tween?.kill();\n              tween = gsap.to(highlight, {\n                autoAlpha: 0,\n                duration: reduced ? 0 : 0.16,\n                overwrite: true,\n              });\n            } else move(true);\n          });\n          const blur = (event: FocusEvent) => blurInContext(event);\n          const observer = new ResizeObserver(() => move(true));\n          observer.observe(root);\n          root\n            .querySelectorAll<HTMLElement>(\"[data-source-target]\")\n            .forEach((target) => observer.observe(target));\n          root.addEventListener(\"pointerover\", enter);\n          root.addEventListener(\"pointerleave\", leave);\n          root.addEventListener(\"focusin\", focus);\n          root.addEventListener(\"focusout\", blur);\n          move(true);\n          return () => {\n            observer.disconnect();\n            root.removeEventListener(\"pointerover\", enter);\n            root.removeEventListener(\"pointerleave\", leave);\n            root.removeEventListener(\"focusin\", focus);\n            root.removeEventListener(\"focusout\", blur);\n            tween?.kill();\n          };\n        },\n        root,\n      );\n      return () => media.revert();\n    },\n    {\n      scope: rootRef,\n      dependencies: [\n        stiffness,\n        damping,\n        speed,\n        expanded,\n        JSON.stringify(sources.map(({ id, href }) => [id, href])),\n      ],\n      revertOnUpdate: true,\n    },\n  );\n\n  const chipClass =\n    \"relative z-10 inline-flex min-h-7 max-w-full items-center gap-1.5 rounded-lg border border-zinc-200/80 bg-zinc-50/60 px-2 py-1 text-[11px] leading-4 text-zinc-600 pointer-coarse:min-h-10 dark:border-white/[0.07] dark:bg-white/[0.025] dark:text-zinc-300\";\n  return (\n    <div\n      ref={rootRef}\n      data-thinking-sources\n      className=\"relative isolate flex flex-wrap items-center gap-1.5\"\n    >\n      <span\n        ref={highlightRef}\n        data-source-highlight\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute top-0 left-0 z-0 rounded-lg bg-zinc-200/80 opacity-0 shadow-[0_2px_8px_-4px_#00000025] dark:bg-white/[0.09]\"\n        style={{ visibility: \"hidden\", transformOrigin: \"center\" }}\n      />\n      {sources.map((source) => {\n        const href = thinkingSourceUrl(source.href);\n        const content = (\n          <>\n            <span\n              data-source-logo\n              aria-hidden=\"true\"\n              className=\"flex size-4 shrink-0 items-center justify-center overflow-hidden rounded-[3px] [&>svg]:size-full\"\n            >\n              {source.icon ?? <Globe2 className=\"size-3.5\" />}\n            </span>\n            <span className=\"truncate\">{source.label}</span>\n          </>\n        );\n        return href ? (\n          <a\n            key={source.id}\n            data-thinking-source={source.id}\n            data-source-target\n            href={href}\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            title={href}\n            className={cn(\n              chipClass,\n              \"outline-none focus-visible:ring-2 focus-visible:ring-violet-400\",\n            )}\n          >\n            {content}\n            <span className=\"sr-only\"> (opens in a new tab)</span>\n          </a>\n        ) : (\n          <span\n            key={source.id}\n            data-thinking-source={source.id}\n            data-source-target\n            className={chipClass}\n          >\n            {content}\n          </span>\n        );\n      })}\n      {remaining > 0 && (\n        <button\n          ref={triggerRef}\n          type=\"button\"\n          data-source-target\n          aria-expanded={showAll}\n          aria-label={\n            showAll ? \"Show fewer sources\" : `Show ${remaining} more sources`\n          }\n          onClick={onToggle}\n          className=\"relative z-10 min-h-7 cursor-pointer rounded-lg px-2 py-1 text-[11px] leading-4 text-zinc-500 outline-none focus-visible:ring-2 focus-visible:ring-violet-400 pointer-coarse:min-h-10 dark:text-zinc-400\"\n        >\n          {showAll ? \"Show less\" : `+${remaining} more`}\n        </button>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/thinking-panel-sources.tsx"
    },
    {
      "name": "thinking-panel-spring.ts",
      "path": "src/registry/ui/thinking-panel-spring.ts",
      "code": "import { calcGeneratorDuration, spring } from \"motion\";\nimport { thinkingPanelBounded } from \"./thinking-panel-types\";\n\nexport function thinkingPanelSpring(\n  stiffness: number,\n  damping: number,\n  speed: number,\n  reduced: boolean,\n) {\n  const response = spring({\n    keyframes: [0, 1],\n    stiffness: thinkingPanelBounded(stiffness, 380, 120, 600),\n    damping: thinkingPanelBounded(damping, 34, 20, 60),\n    restSpeed: 0.001,\n    restDelta: 0.001,\n  });\n  const settle = calcGeneratorDuration(response);\n  return {\n    duration: reduced\n      ? 0\n      : settle / 1000 / thinkingPanelBounded(speed, 1, 0.5, 2),\n    ease: (progress: number) =>\n      progress === 1 ? 1 : response.next(progress * settle).value,\n  };\n}\n",
      "content": "import { calcGeneratorDuration, spring } from \"motion\";\nimport { thinkingPanelBounded } from \"./thinking-panel-types\";\n\nexport function thinkingPanelSpring(\n  stiffness: number,\n  damping: number,\n  speed: number,\n  reduced: boolean,\n) {\n  const response = spring({\n    keyframes: [0, 1],\n    stiffness: thinkingPanelBounded(stiffness, 380, 120, 600),\n    damping: thinkingPanelBounded(damping, 34, 20, 60),\n    restSpeed: 0.001,\n    restDelta: 0.001,\n  });\n  const settle = calcGeneratorDuration(response);\n  return {\n    duration: reduced\n      ? 0\n      : settle / 1000 / thinkingPanelBounded(speed, 1, 0.5, 2),\n    ease: (progress: number) =>\n      progress === 1 ? 1 : response.next(progress * settle).value,\n  };\n}\n",
      "type": "registry:ui",
      "target": "@ui/thinking-panel-spring.ts"
    }
  ]
}
