{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"agent-activity","type":"registry:component","title":"Agent Activity","description":"One adaptive activity stream for reasoning, searches, tool calls, structured execution traces, or a chronological mix.","author":"Saurabh <saurabh10102@gmail.com>","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/agents/agent-activity/index.tsx","type":"registry:component","target":"@components/agents/agent-activity/index.tsx","content":"\"use client\";\n// beui.dev/components/agents/agent-activity\n\nimport { ChevronDown } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { ThinkingShimmer } from \"@/components/agents/loading-states/thinking-shimmer\";\nimport { AgentDisclosure } from \"@/components/agents/agent-disclosure\";\nimport {\n  EASE_OUT,\n  SPRING_LAYOUT,\n  SPRING_SWAP,\n} from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\nimport { ActivityRow } from \"./activity-row\";\nimport type {\n  AgentActivityContentType,\n  AgentActivityItem,\n  AgentActivityProps,\n} from \"./types\";\n\nexport type {\n  AgentActivityContentType,\n  AgentActivityItem,\n  AgentActivityProps,\n  AgentActivitySearch,\n  AgentActivityStatus,\n  AgentActivityStep,\n  AgentActivityText,\n  AgentActivityTool,\n  AgentActivityTrace,\n  AgentSearchResult,\n  AgentStepStatus,\n  AgentTraceKind,\n} from \"./types\";\n\nfunction formatDuration(duration: number) {\n  const seconds = Math.max(0, Math.round(duration));\n  if (seconds < 60) return `${seconds}s`;\n\n  const minutes = Math.floor(seconds / 60);\n  const remainder = seconds % 60;\n  return remainder === 0 ? `${minutes}m` : `${minutes}m ${remainder}s`;\n}\n\nfunction useControllableOpen({\n  open,\n  defaultOpen,\n  onOpenChange,\n}: {\n  open?: boolean;\n  defaultOpen: boolean;\n  onOpenChange?: (open: boolean) => void;\n}) {\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const controlled = open !== undefined;\n  const currentOpen = open ?? internalOpen;\n\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (!controlled) setInternalOpen(next);\n      onOpenChange?.(next);\n    },\n    [controlled, onOpenChange],\n  );\n\n  return [currentOpen, setOpen] as const;\n}\n\nfunction getContentType(items: AgentActivityItem[]): AgentActivityContentType {\n  const first = items[0]?.type;\n  return first && items.every((item) => item.type === first) ? first : \"mixed\";\n}\n\nfunction getActiveLabel(type: AgentActivityContentType) {\n  if (type === \"search\") return \"Searching the web…\";\n  if (type === \"tool\") return \"Running tools…\";\n  if (type === \"trace\") return \"Working through the run…\";\n  if (type === \"mixed\") return \"Working through it…\";\n  return \"Thinking…\";\n}\n\nfunction getSummary(\n  type: AgentActivityContentType,\n  items: AgentActivityItem[],\n  duration: number,\n): ReactNode {\n  if (type === \"step\" || type === \"text\") {\n    return (\n      <>\n        Thought for <span className=\"tabular-nums\">{formatDuration(duration)}</span>\n      </>\n    );\n  }\n  if (type === \"search\") return \"Searched the web\";\n  if (type === \"tool\") {\n    return `Ran ${items.length} ${items.length === 1 ? \"tool\" : \"tools\"}`;\n  }\n  if (type === \"trace\") {\n    const messages = items.filter(\n      (item) =>\n        item.type === \"trace\" &&\n        (item.kind === \"thinking\" || item.kind === \"message\"),\n    ).length;\n    const tools = items.length - messages;\n    return `${tools} ${tools === 1 ? \"tool call\" : \"tool calls\"}, ${messages} ${messages === 1 ? \"message\" : \"messages\"}`;\n  }\n  return `Completed ${items.length} ${items.length === 1 ? \"step\" : \"steps\"}`;\n}\n\nexport function AgentActivity({\n  items,\n  contentType: initialContentType,\n  status = \"working\",\n  duration = 0,\n  open,\n  defaultOpen = false,\n  onOpenChange,\n  collapseOnComplete = true,\n  activeLabel,\n  summary,\n  maxHeight = 208,\n  className,\n  contentClassName,\n}: AgentActivityProps) {\n  const reduce = useReducedMotion() ?? false;\n  const baseId = useId();\n  const triggerId = `${baseId}-trigger`;\n  const contentId = `${baseId}-content`;\n  const contentRef = useRef<HTMLDivElement>(null);\n  const viewportRef = useRef<HTMLDivElement>(null);\n  const previousStatus = useRef(status);\n  const [contentHeight, setContentHeight] = useState(0);\n  const [currentOpen, setOpen] = useControllableOpen({\n    open,\n    defaultOpen,\n    onOpenChange,\n  });\n  const working = status === \"working\";\n  const expanded = working || currentOpen;\n  const contentType = items.length\n    ? getContentType(items)\n    : (initialContentType ?? \"mixed\");\n  const cappedHeight = Math.min(contentHeight, Math.max(0, maxHeight));\n  const viewportHeight = working ? Math.max(0, maxHeight) : cappedHeight;\n  const capped = contentHeight > maxHeight;\n  const streamOffset = working\n    ? Math.min(0, viewportHeight - contentHeight)\n    : 0;\n\n  useLayoutEffect(() => {\n    const node = contentRef.current;\n    if (!node) return;\n\n    const measure = () => setContentHeight(node.offsetHeight);\n    measure();\n\n    if (typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(measure);\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, []);\n\n  useEffect(() => {\n    if (previousStatus.current === \"working\" && status === \"complete\") {\n      setOpen(!collapseOnComplete);\n    }\n    previousStatus.current = status;\n  }, [collapseOnComplete, setOpen, status]);\n\n  const toggle = () => {\n    const next = !currentOpen;\n    setOpen(next);\n    if (next) requestAnimationFrame(() => viewportRef.current?.scrollTo({ top: 0 }));\n  };\n\n  const liveLabel = activeLabel ?? getActiveLabel(contentType);\n  const completedSummary = summary ?? getSummary(contentType, items, duration);\n  const maskImage = capped\n    ? working\n      ? \"linear-gradient(to bottom, transparent, black 12px)\"\n      : \"linear-gradient(to bottom, transparent, black 12px, black calc(100% - 12px), transparent)\"\n    : undefined;\n\n  return (\n    <div\n      data-state={working ? \"working\" : expanded ? \"open\" : \"closed\"}\n      data-content={contentType}\n      aria-busy={working}\n      className={cn(\"w-full text-sm\", className)}\n    >\n      {working ? (\n        <div\n          id={triggerId}\n          role=\"status\"\n          className=\"flex h-7 min-w-0 items-center text-muted-foreground\"\n        >\n          <ThinkingShimmer>{liveLabel}</ThinkingShimmer>\n        </div>\n      ) : (\n        <button\n          id={triggerId}\n          type=\"button\"\n          aria-expanded={expanded}\n          aria-controls={contentId}\n          onClick={toggle}\n          className=\"group flex h-7 min-w-0 items-center gap-1.5 rounded-md text-left font-medium text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n        >\n          <span className=\"truncate\">{completedSummary}</span>\n          <motion.span\n            aria-hidden=\"true\"\n            animate={{ rotate: expanded ? 180 : 0 }}\n            transition={reduce ? { duration: 0 } : SPRING_SWAP}\n            className=\"inline-flex shrink-0 text-muted-foreground/70 group-hover:text-foreground\"\n          >\n            <ChevronDown className=\"size-3.5\" />\n          </motion.span>\n        </button>\n      )}\n\n      <AgentDisclosure\n        id={contentId}\n        role=\"region\"\n        aria-labelledby={triggerId}\n        open={expanded}\n        openHeight={viewportHeight}\n      >\n        <div\n          ref={viewportRef}\n          className={cn(\n            \"scrollbar-hide pr-1\",\n            capped && expanded && !working ? \"overflow-y-auto\" : \"overflow-y-hidden\",\n          )}\n          style={{ height: viewportHeight, maskImage, WebkitMaskImage: maskImage }}\n        >\n          <motion.div\n            ref={contentRef}\n            role=\"list\"\n            initial={false}\n            animate={{ y: streamOffset }}\n            transition={reduce ? { duration: 0 } : SPRING_LAYOUT}\n            className={cn(\"space-y-0.5 py-2\", contentClassName)}\n          >\n            <AnimatePresence mode=\"popLayout\">\n              {items.map((item) => (\n                <motion.div\n                  layout=\"position\"\n                  key={item.id}\n                  role=\"listitem\"\n                  initial={reduce ? { opacity: 1 } : { opacity: 0, y: 6 }}\n                  animate={{ opacity: 1, y: 0 }}\n                  exit={reduce ? { opacity: 0 } : { opacity: 0, y: -3 }}\n                  transition={\n                    reduce\n                      ? { duration: 0 }\n                      : {\n                          opacity: { duration: 0.18, ease: EASE_OUT },\n                          y: SPRING_LAYOUT,\n                          layout: SPRING_LAYOUT,\n                        }\n                  }\n                >\n                  <ActivityRow item={item} />\n                </motion.div>\n              ))}\n            </AnimatePresence>\n          </motion.div>\n        </div>\n      </AgentDisclosure>\n    </div>\n  );\n}\n"},{"path":"components/agents/agent-activity/activity-row.tsx","type":"registry:component","target":"@components/agents/agent-activity/activity-row.tsx","content":"import {\n  Check,\n  Circle,\n  FileText,\n  Globe2,\n  ImageIcon,\n  MessageSquare,\n  PencilLine,\n  Search,\n  Sparkles,\n  SquareTerminal,\n  Wrench,\n} from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { EASE_OUT, SPRING_LAYOUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\nimport type {\n  AgentActivityItem,\n  AgentActivitySearch,\n  AgentActivityStep,\n  AgentActivityText,\n  AgentActivityTool,\n  AgentActivityTrace,\n  AgentSearchResult,\n} from \"./types\";\n\nfunction StepRow({ item }: { item: AgentActivityStep }) {\n  const state = item.status ?? \"complete\";\n\n  return (\n    <div className=\"flex min-h-7 items-start gap-2.5 rounded-md px-1.5 py-1\">\n      <span\n        aria-hidden=\"true\"\n        className=\"mt-0.5 grid size-4 shrink-0 place-items-center text-muted-foreground/70\"\n      >\n        {state === \"complete\" ? (\n          <Check className=\"size-4\" strokeWidth={1.8} />\n        ) : state === \"active\" ? (\n          <span className=\"relative grid size-3 place-items-center\">\n            <motion.span\n              className=\"absolute inset-0 rounded-full bg-foreground/10\"\n              animate={{ opacity: [0.35, 0.8, 0.35] }}\n              transition={{ duration: 1.5, repeat: Number.POSITIVE_INFINITY }}\n            />\n            <span className=\"size-1.5 rounded-full bg-foreground/60\" />\n          </span>\n        ) : (\n          <Circle className=\"size-3\" strokeWidth={1.5} />\n        )}\n      </span>\n      <span\n        className={cn(\n          \"min-w-0 flex-1 leading-5\",\n          state === \"pending\" ? \"text-muted-foreground/55\" : \"text-foreground/90\",\n        )}\n      >\n        {item.label}\n      </span>\n      {item.meta ? (\n        <span className=\"shrink-0 leading-5 text-muted-foreground/55\">\n          {item.meta}\n        </span>\n      ) : null}\n    </div>\n  );\n}\n\nfunction TextRow({ item }: { item: AgentActivityText }) {\n  return (\n    <div className=\"rounded-md px-1.5 py-1 leading-5 text-muted-foreground\">\n      {item.content}\n    </div>\n  );\n}\n\nfunction SearchResultRow({\n  result,\n}: {\n  result: AgentSearchResult;\n}) {\n  const content = (\n    <>\n      <span\n        aria-hidden=\"true\"\n        className=\"grid size-5 shrink-0 place-items-center text-muted-foreground\"\n      >\n        {result.icon ?? <Globe2 className=\"size-3\" strokeWidth={2} />}\n      </span>\n      <span className=\"min-w-0 truncate font-medium text-foreground/90\">\n        {result.title}\n      </span>\n      {result.domain ? (\n        <span className=\"min-w-0 truncate text-muted-foreground/55\">\n          {result.domain}\n        </span>\n      ) : null}\n    </>\n  );\n  const className = cn(\n    \"flex min-h-7 items-center gap-2 rounded-md px-1.5 py-1 text-left outline-none transition-colors\",\n    result.url && \"focus-visible:ring-2 focus-visible:ring-ring\",\n  );\n\n  return result.url ? (\n    <a href={result.url} className={className}>\n      {content}\n    </a>\n  ) : (\n    <div className={className}>{content}</div>\n  );\n}\n\nfunction SearchRow({ item }: { item: AgentActivitySearch }) {\n  const reduce = useReducedMotion() ?? false;\n  const enter = reduce ? { opacity: 1 } : { opacity: 0, y: 6 };\n  const visible = { opacity: 1, y: 0 };\n  const exit = reduce ? { opacity: 0 } : { opacity: 0, y: -3 };\n  const transition = reduce\n    ? { duration: 0 }\n    : {\n        opacity: { duration: 0.18, ease: EASE_OUT },\n        y: SPRING_LAYOUT,\n        layout: SPRING_LAYOUT,\n      };\n\n  return (\n    <div className=\"space-y-0.5\">\n      <div className=\"flex min-h-7 items-center gap-2.5 rounded-md px-1.5 py-1 text-muted-foreground\">\n        <Search aria-hidden=\"true\" className=\"size-4 shrink-0\" strokeWidth={1.7} />\n        <span className=\"min-w-0 truncate\">{item.query}</span>\n      </div>\n      {item.results?.length ? (\n        <div className=\"space-y-0.5 pl-4\">\n          <AnimatePresence initial mode=\"popLayout\">\n            {item.results.map((result) => (\n              <motion.div\n                layout=\"position\"\n                key={result.id}\n                initial={enter}\n                animate={visible}\n                exit={exit}\n                transition={transition}\n              >\n                <SearchResultRow result={result} />\n              </motion.div>\n            ))}\n          </AnimatePresence>\n        </div>\n      ) : null}\n      <AnimatePresence initial>\n        {item.moreCount ? (\n          <motion.div\n            key=\"more-results\"\n            initial={enter}\n            animate={visible}\n            exit={exit}\n            transition={transition}\n            className=\"px-1.5 py-1 pl-8 text-muted-foreground/55\"\n          >\n            +{item.moreCount} more\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nfunction ActionIcon({ action }: { action: string }) {\n  if (action === \"read\") return <FileText className=\"size-4\" />;\n  if (action === \"edit\" || action === \"write\") {\n    return <PencilLine className=\"size-4\" />;\n  }\n  if (action === \"run\") return <SquareTerminal className=\"size-4\" />;\n  return <Wrench className=\"size-4\" />;\n}\n\nfunction ToolRow({ item }: { item: AgentActivityTool }) {\n  const action = item.action.charAt(0).toUpperCase() + item.action.slice(1);\n\n  return (\n    <div className=\"flex min-h-8 min-w-0 items-center gap-2.5 rounded-md px-1.5 py-0.5 leading-5\">\n      <span\n        aria-hidden=\"true\"\n        className=\"grid size-4 shrink-0 place-items-center text-muted-foreground/70\"\n      >\n        <ActionIcon action={item.action} />\n      </span>\n      <span className=\"shrink-0 font-medium text-foreground/90\">{action}</span>\n      <span className=\"min-w-0 flex-1 truncate rounded-lg bg-muted/80 px-2.5 py-1 font-mono text-xs text-muted-foreground/70\">\n        {item.target}\n      </span>\n      {typeof item.additions === \"number\" || typeof item.deletions === \"number\" ? (\n        <span className=\"flex shrink-0 items-center gap-2 font-mono tabular-nums\">\n          {typeof item.additions === \"number\" ? (\n            <span className=\"text-emerald-500\">+{item.additions}</span>\n          ) : null}\n          {typeof item.deletions === \"number\" ? (\n            <span className=\"text-rose-500\">−{item.deletions}</span>\n          ) : null}\n        </span>\n      ) : null}\n    </div>\n  );\n}\n\nfunction TraceIcon({ kind }: { kind: AgentActivityTrace[\"kind\"] }) {\n  if (kind === \"thinking\") return <Sparkles className=\"size-4\" />;\n  if (kind === \"message\") return <MessageSquare className=\"size-4\" />;\n  if (kind === \"write\") return <PencilLine className=\"size-4\" />;\n  if (kind === \"run\") return <SquareTerminal className=\"size-4\" />;\n  if (kind === \"read\") return <ImageIcon className=\"size-4\" />;\n  return <Wrench className=\"size-4\" />;\n}\n\nfunction TraceRow({ item }: { item: AgentActivityTrace }) {\n  return (\n    <div className=\"grid min-h-8 grid-cols-[1rem_auto_minmax(0,1fr)] items-center gap-2.5 rounded-md px-1.5 py-0.5\">\n      <span\n        aria-hidden=\"true\"\n        className=\"grid size-4 place-items-center text-muted-foreground/70\"\n      >\n        {item.icon ?? <TraceIcon kind={item.kind} />}\n      </span>\n      <span className=\"font-medium text-foreground/90\">{item.label}</span>\n      {item.detail ? (\n        <span className=\"min-w-0 truncate rounded-lg bg-muted/80 px-2.5 py-1 font-mono text-xs text-muted-foreground/70\">\n          {item.detail}\n        </span>\n      ) : (\n        <span />\n      )}\n    </div>\n  );\n}\n\nexport function ActivityRow({ item }: { item: AgentActivityItem }) {\n  if (item.type === \"text\") return <TextRow item={item} />;\n  if (item.type === \"search\") return <SearchRow item={item} />;\n  if (item.type === \"tool\") return <ToolRow item={item} />;\n  if (item.type === \"trace\") return <TraceRow item={item} />;\n  return <StepRow item={item} />;\n}\n"},{"path":"components/agents/agent-activity/types.ts","type":"registry:component","target":"@components/agents/agent-activity/types.ts","content":"import type { ReactNode } from \"react\";\n\nexport type AgentActivityStatus = \"working\" | \"complete\";\nexport type AgentStepStatus = \"pending\" | \"active\" | \"complete\";\n\nexport interface AgentActivityStep {\n  id: string;\n  type: \"step\";\n  label: ReactNode;\n  status?: AgentStepStatus;\n  meta?: ReactNode;\n}\n\nexport interface AgentActivityText {\n  id: string;\n  type: \"text\";\n  content: ReactNode;\n}\n\nexport interface AgentSearchResult {\n  id: string;\n  title: ReactNode;\n  domain?: ReactNode;\n  url?: string;\n  icon?: ReactNode;\n}\n\nexport interface AgentActivitySearch {\n  id: string;\n  type: \"search\";\n  query: ReactNode;\n  results?: AgentSearchResult[];\n  moreCount?: number;\n}\n\nexport interface AgentActivityTool {\n  id: string;\n  type: \"tool\";\n  action: \"read\" | \"edit\" | \"run\" | (string & {});\n  target: ReactNode;\n  additions?: number;\n  deletions?: number;\n}\n\nexport type AgentTraceKind =\n  | \"thinking\"\n  | \"message\"\n  | \"write\"\n  | \"run\"\n  | \"read\"\n  | (string & {});\n\nexport interface AgentActivityTrace {\n  id: string;\n  type: \"trace\";\n  kind: AgentTraceKind;\n  label: ReactNode;\n  detail?: ReactNode;\n  icon?: ReactNode;\n}\n\nexport type AgentActivityItem =\n  | AgentActivityStep\n  | AgentActivityText\n  | AgentActivitySearch\n  | AgentActivityTool\n  | AgentActivityTrace;\n\nexport type AgentActivityContentType = AgentActivityItem[\"type\"] | \"mixed\";\n\nexport interface AgentActivityProps {\n  /** Chronological activity entries. Append or update items as events stream. */\n  items: AgentActivityItem[];\n  /** Expected activity kind before the first streamed item arrives. */\n  contentType?: AgentActivityContentType;\n  /** Current run phase. Active runs always stay expanded. */\n  status?: AgentActivityStatus;\n  /** Elapsed run time, in seconds. Used by the step-only summary. */\n  duration?: number;\n  /** Controlled expanded state used after the run completes. */\n  open?: boolean;\n  /** Initial expanded state used after the run completes. */\n  defaultOpen?: boolean;\n  /** Called when the completed activity disclosure changes state. */\n  onOpenChange?: (open: boolean) => void;\n  /** Collapse the disclosure when status changes from working to complete. */\n  collapseOnComplete?: boolean;\n  /** Optional label shown while the run is active. */\n  activeLabel?: ReactNode;\n  /** Optional completed summary. Derived from the item types by default. */\n  summary?: ReactNode;\n  /** Maximum visible activity height before the stream begins gliding. */\n  maxHeight?: number;\n  className?: string;\n  contentClassName?: string;\n}\n"},{"path":"components/agents/agent-disclosure.tsx","type":"registry:component","target":"@components/agents/agent-disclosure.tsx","content":"\"use client\";\n\nimport { motion, type HTMLMotionProps, useReducedMotion } from \"motion/react\";\nimport type { CSSProperties } from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface AgentDisclosureProps\n  extends Omit<HTMLMotionProps<\"div\">, \"animate\" | \"initial\"> {\n  open: boolean;\n  openHeight?: CSSProperties[\"height\"];\n}\n\n/** Shared transform-only reveal for collapsible agent content. */\nexport function AgentDisclosure({\n  open,\n  openHeight = \"auto\",\n  className,\n  style,\n  transition,\n  ...props\n}: AgentDisclosureProps) {\n  const reduce = useReducedMotion() ?? false;\n\n  return (\n    <motion.div\n      {...props}\n      aria-hidden={!open}\n      inert={!open}\n      initial={false}\n      animate={\n        reduce\n          ? { opacity: open ? 1 : 0 }\n          : {\n              opacity: open ? 1 : 0,\n              clipPath: open ? \"inset(0 0 0% 0)\" : \"inset(0 0 100% 0)\",\n              y: open ? 0 : -4,\n            }\n      }\n      transition={\n        transition ?? {\n          duration: reduce ? 0 : open ? 0.22 : 0.14,\n          ease: EASE_OUT,\n        }\n      }\n      className={cn(\"overflow-hidden\", className)}\n      style={{\n        ...style,\n        height: open ? openHeight : 0,\n        pointerEvents: open ? undefined : \"none\",\n        transformOrigin: \"top\",\n      }}\n    />\n  );\n}\n"},{"path":"components/agents/loading-states/thinking-shimmer.tsx","type":"registry:component","target":"@components/agents/loading-states/thinking-shimmer.tsx","content":"import type { ReactNode } from \"react\";\nimport { TextShimmer } from \"@/components/motion/text-shimmer\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface ThinkingShimmerProps {\n  /** Loading message shown to the user. */\n  children?: ReactNode;\n  /** Seconds taken for one shimmer pass. */\n  duration?: number;\n  className?: string;\n}\n\nexport function ThinkingShimmer({\n  children = \"Thinking…\",\n  duration = 1.8,\n  className,\n}: ThinkingShimmerProps) {\n  return (\n    <TextShimmer\n      as=\"span\"\n      duration={duration}\n      className={cn(\"font-medium\", className)}\n    >\n      {children}\n    </TextShimmer>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"},{"path":"components/motion/text-shimmer.tsx","type":"registry:component","target":"@components/motion/text-shimmer.tsx","content":"import { cn } from \"@/lib/utils\";\nimport type { ElementType, ReactNode } from \"react\";\nimport {\n  TEXT_SHIMMER_CLASS_NAME,\n  TEXT_SHIMMER_KEYFRAMES,\n  textShimmerStyle,\n} from \"@/lib/text-shimmer\";\n\nexport interface TextShimmerProps {\n  children: ReactNode;\n  as?: ElementType;\n  duration?: number;\n  className?: string;\n}\n\nexport function TextShimmer({ children, as: Comp = \"span\", duration = 2.5, className }: TextShimmerProps) {\n  return (\n    <>\n      <style>\n        {TEXT_SHIMMER_KEYFRAMES}\n      </style>\n      <Comp\n        style={textShimmerStyle(duration)}\n        className={cn(\n          \"inline-block\",\n          TEXT_SHIMMER_CLASS_NAME,\n          className,\n        )}\n      >\n        {children}\n      </Comp>\n    </>\n  );\n}\n"},{"path":"lib/text-shimmer.ts","type":"registry:lib","target":"@lib/text-shimmer.ts","content":"import type { CSSProperties } from \"react\";\n\nexport const TEXT_SHIMMER_KEYFRAMES =\n  \"@keyframes beui-text-shimmer{from{background-position:200% 0}to{background-position:-200% 0}}\";\n\nexport const TEXT_SHIMMER_CLASS_NAME =\n  \"bg-[length:200%_100%] bg-clip-text text-transparent bg-[linear-gradient(110deg,var(--muted-foreground)_30%,var(--foreground)_50%,var(--muted-foreground)_70%)]\";\n\nexport function textShimmerStyle(duration: number): CSSProperties {\n  return {\n    animation: `beui-text-shimmer ${duration}s linear infinite`,\n  };\n}\n"}]}