{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "thinking-panel",
  "type": "registry:ui",
  "title": "Thinking Panel",
  "description": "An AI activity panel with a live timer, expandable source chips and spring-driven progress steps.",
  "dependencies": [
    "gsap",
    "@gsap/react",
    "motion",
    "lucide-react",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/thinking-panel.tsx",
      "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"
    },
    {
      "path": "src/registry/ui/thinking-panel-types.ts",
      "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"
    },
    {
      "path": "src/registry/ui/use-thinking-panel-motion.ts",
      "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"
    },
    {
      "path": "src/registry/ui/thinking-panel-sources.tsx",
      "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"
    },
    {
      "path": "src/registry/ui/thinking-panel-spring.ts",
      "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"
    }
  ]
}
