{"slug":"message","name":"Message","description":"Composable conversation primitives for message rows, grouped bubbles, avatars, metadata, live markers, and a mount-only trailing-edge pop-up for newly sent rows.","category":"agents","source_url":"https://beui.dev/r/message/raw","detail_url":"https://beui.dev/r/message","raw_url":"https://beui.dev/r/message/raw","page_url":"https://beui.dev/components/agents/message","markdown_url":"https://beui.dev/components/agents/message.md","published_at":"2026-08-02","updated_at":"2026-09-26","dependencies":["clsx","lucide-react","motion","react","react-dom","tailwind-merge"],"internal":["../magnetic","./base","./magnetic","./metallic","./stateful","@/components/agents/agent-disclosure","@/components/agents/citations","@/components/agents/message","@/components/agents/message-bubble","@/components/agents/message-context","@/components/agents/message-scroller","@/components/agents/prompt-input","@/components/agents/streaming-response","@/components/motion/button","@/components/motion/popover-morph","@/components/motion/popover-position","@/components/motion/preview-rail","@/components/motion/select","@/components/previews/agents/chat-preview","@/lib/ease","@/lib/favicon","@/lib/hooks/use-dismiss","@/lib/hooks/use-favicon","@/lib/hooks/use-hover-capable","@/lib/hooks/use-hover-gesture","@/lib/hooks/use-tap-gesture","@/lib/touch","@/lib/utils"],"files":[{"path":"components/agents/message.tsx","type":"component","content":"\"use client\";\n// beui.dev/components/agents/message\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport {\n  type ComponentPropsWithRef,\n  createContext,\n  type ReactNode,\n  useContext,\n} from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\nimport { MessageSideContext } from \"@/components/agents/message-context\";\n\nexport {\n  MessageBubble,\n  MessageBubbleCollapsible,\n  MessageBubbleContent,\n  MessageBubbleGroup,\n} from \"@/components/agents/message-bubble\";\nexport { MessageScroller } from \"@/components/agents/message-scroller\";\nexport type { MessageScrollerProps } from \"@/components/agents/message-scroller\";\n\nexport type MessageFrom = \"user\" | \"assistant\";\n\ninterface MessageContextValue {\n  from: MessageFrom;\n}\n\nconst MessageContext = createContext<MessageContextValue>({\n  from: \"assistant\",\n});\n\nexport interface MessageProps\n  extends Omit<ComponentPropsWithRef<typeof motion.article>, \"children\"> {\n  from: MessageFrom;\n  /** Plays a trailing-edge pop-up once when this message row mounts. */\n  animateIn?: boolean;\n  children: ReactNode;\n}\n\nexport interface MessageGroupProps extends ComponentPropsWithRef<\"div\"> {\n  spacing?: \"compact\" | \"default\";\n}\n\nexport interface MessageAvatarProps extends ComponentPropsWithRef<\"div\"> {\n  /** Keep an empty avatar slot so grouped messages remain aligned. */\n  placeholder?: boolean;\n}\n\nexport type MessageContentProps = ComponentPropsWithRef<\"div\">;\nexport type MessageHeaderProps = ComponentPropsWithRef<\"div\">;\nexport type MessageFooterProps = ComponentPropsWithRef<\"div\">;\n\nexport type MessageMarkerProps = ComponentPropsWithRef<\"div\">;\n\nexport interface MessageTypingProps extends ComponentPropsWithRef<\"span\"> {\n  label?: string;\n}\n\n// A sent row should rise from the live edge without changing measured layout.\nconst MESSAGE_POP_UP = {\n  type: \"spring\",\n  stiffness: 480,\n  damping: 32,\n  mass: 0.62,\n} as const;\n\nexport function Message({\n  from,\n  animateIn = false,\n  children,\n  className,\n  initial,\n  animate,\n  transition,\n  exit,\n  style,\n  ...props\n}: MessageProps) {\n  const reduce = useReducedMotion() ?? false;\n\n  return (\n    <MessageSideContext.Provider value={from === \"user\" ? \"end\" : \"start\"}>\n      <MessageContext.Provider value={{ from }}>\n        <motion.article\n          data-slot=\"message\"\n          data-from={from}\n          aria-label={props[\"aria-label\"] ?? `${from} message`}\n          initial={\n            initial ??\n            (animateIn && !reduce\n              ? {\n                  opacity: 0,\n                  transform: \"translateY(8px) scale(0.95)\",\n                }\n              : false)\n          }\n          animate={\n            animate ??\n            (animateIn && !reduce\n              ? {\n                  opacity: 1,\n                  transform: \"translateY(0px) scale(1)\",\n                }\n              : { opacity: 1 })\n          }\n          exit={\n            exit ??\n            (reduce\n              ? { opacity: 0 }\n              : {\n                  opacity: 0,\n                  transform: \"translateY(-3px) scale(0.99)\",\n                })\n          }\n          transition={\n            transition ?? (reduce ? { duration: 0.12 } : MESSAGE_POP_UP)\n          }\n          style={{\n            transformOrigin: from === \"user\" ? \"100% 100%\" : \"0% 100%\",\n            ...style,\n          }}\n          className={cn(\n            \"group/message flex w-full items-start gap-2\",\n            from === \"user\" ? \"flex-row-reverse\" : \"flex-row\",\n            className,\n          )}\n          {...props}\n        >\n          {children}\n        </motion.article>\n      </MessageContext.Provider>\n    </MessageSideContext.Provider>\n  );\n}\n\nexport function MessageGroup({\n  spacing = \"compact\",\n  className,\n  ...props\n}: MessageGroupProps) {\n  return (\n    <div\n      data-slot=\"message-group\"\n      className={cn(\n        \"flex w-full flex-col\",\n        spacing === \"compact\" ? \"gap-1.5\" : \"gap-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function MessageAvatar({\n  placeholder = false,\n  children,\n  className,\n  ...props\n}: MessageAvatarProps) {\n  return (\n    <div\n      data-slot=\"message-avatar\"\n      aria-hidden={placeholder || undefined}\n      className={cn(\n        \"grid size-7 shrink-0 place-items-center overflow-hidden rounded-full bg-muted text-xs font-medium text-muted-foreground [&_img]:size-full [&_img]:object-cover [&_svg]:size-3.5\",\n        placeholder && \"invisible\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function MessageContent({ className, ...props }: MessageContentProps) {\n  const { from } = useContext(MessageContext);\n\n  return (\n    <div\n      data-slot=\"message-content\"\n      className={cn(\n        \"flex min-w-0 flex-1 flex-col gap-1.5\",\n        from === \"user\" ? \"items-end\" : \"items-start\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function MessageHeader({ className, ...props }: MessageHeaderProps) {\n  const { from } = useContext(MessageContext);\n\n  return (\n    <div\n      data-slot=\"message-header\"\n      className={cn(\n        \"flex items-center gap-1.5 px-1 text-[11px] leading-none text-muted-foreground\",\n        from === \"user\" ? \"justify-end\" : \"justify-start\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function MessageFooter({ className, ...props }: MessageFooterProps) {\n  const { from } = useContext(MessageContext);\n\n  return (\n    <div\n      data-slot=\"message-footer\"\n      className={cn(\n        \"flex min-h-5 items-center gap-1 px-1 text-[11px] text-muted-foreground\",\n        from === \"user\" ? \"justify-end\" : \"justify-start\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function MessageMarker({ className, ...props }: MessageMarkerProps) {\n  return (\n    <div\n      data-slot=\"message-marker\"\n      className={cn(\n        \"mx-auto flex w-fit max-w-[88%] items-center gap-1.5 rounded-full bg-muted/70 px-2.5 py-1 text-center text-xs text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function MessageTyping({\n  label = \"Responding\",\n  className,\n  ...props\n}: MessageTypingProps) {\n  const reduce = useReducedMotion() ?? false;\n\n  return (\n    <span\n      data-slot=\"message-typing\"\n      className={cn(\"inline-flex h-5 items-center gap-1\", className)}\n      {...props}\n    >\n      <span className=\"sr-only\">{label}</span>\n      {[0, 1, 2].map((index) => (\n        <motion.span\n          key={index}\n          aria-hidden=\"true\"\n          className=\"size-1 rounded-full bg-current\"\n          animate={\n            reduce\n              ? { opacity: 0.45 }\n              : { opacity: [0.28, 0.85, 0.28], y: [0, -2, 0] }\n          }\n          transition={{\n            duration: 1.05,\n            ease: EASE_OUT,\n            repeat: Number.POSITIVE_INFINITY,\n            delay: index * 0.14,\n          }}\n        />\n      ))}\n    </span>\n  );\n}\n"},{"path":"components/agents/message-bubble.tsx","type":"util","content":"\"use client\";\n\nimport { ChevronDown } from \"lucide-react\";\nimport {\n  type HTMLMotionProps,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  cloneElement,\n  type ComponentPropsWithRef,\n  createContext,\n  type ReactElement,\n  type ReactNode,\n  type Ref,\n  useCallback,\n  useContext,\n  useId,\n  useState,\n} from \"react\";\nimport {\n  EASE_OUT,\n  SPRING_LAYOUT,\n  SPRING_SWAP,\n} from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\nimport { MessageSideContext } from \"@/components/agents/message-context\";\n\nexport type MessageBubbleVariant =\n  | \"solid\"\n  | \"soft\"\n  | \"tint\"\n  | \"outline\"\n  | \"ghost\"\n  | \"danger\";\nexport type MessageBubbleAlign = \"start\" | \"end\";\n\ninterface MessageBubbleContextValue {\n  align?: MessageBubbleAlign;\n  animateIn: boolean;\n  variant: MessageBubbleVariant;\n}\n\nconst MessageBubbleContext = createContext<MessageBubbleContextValue>({\n  animateIn: true,\n  variant: \"soft\",\n});\nconst MessageBubbleLayoutContext = createContext<() => void>(() => {});\n\nexport interface MessageBubbleProps\n  extends Omit<HTMLMotionProps<\"div\">, \"children\"> {\n  variant?: MessageBubbleVariant;\n  /** Defaults to the surrounding Message alignment when omitted. */\n  align?: MessageBubbleAlign;\n  /** Plays the bubble entrance once when this component mounts. */\n  animateIn?: boolean;\n  children?: ReactNode;\n}\n\nexport interface MessageBubbleContentProps\n  extends ComponentPropsWithRef<\"div\"> {\n  /** Replaces the content element while preserving bubble styling. */\n  render?: ReactElement;\n}\n\nexport interface MessageBubbleGroupProps extends ComponentPropsWithRef<\"div\"> {\n  spacing?: \"compact\" | \"default\";\n}\n\nexport interface MessageBubbleCollapsibleProps\n  extends ComponentPropsWithRef<\"div\"> {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  collapsedLines?: 2 | 3 | 4 | 5 | 6;\n  moreLabel?: ReactNode;\n  lessLabel?: ReactNode;\n  contentClassName?: string;\n  triggerClassName?: string;\n  children?: ReactNode;\n}\n\nfunction mergeRefs<T>(...refs: Array<Ref<T> | undefined>) {\n  return (node: T | null) => {\n    for (const ref of refs) {\n      if (typeof ref === \"function\") ref(node);\n      else if (ref) ref.current = node;\n    }\n  };\n}\n\nconst BUBBLE_CONTENT_REVEAL = {\n  duration: 0.12,\n  ease: EASE_OUT,\n  delay: 0.04,\n} as const;\n\n// Sent bubbles should pop into place quickly with one restrained overshoot.\nconst BUBBLE_POP = {\n  type: \"spring\",\n  stiffness: 520,\n  damping: 27,\n  mass: 0.52,\n} as const;\n\nexport function MessageBubble({\n  variant = \"soft\",\n  align,\n  animateIn = false,\n  className,\n  children,\n  initial,\n  animate,\n  exit,\n  transition,\n  layout,\n  ...props\n}: MessageBubbleProps) {\n  const reduce = useReducedMotion() ?? false;\n  const messageSide = useContext(MessageSideContext);\n  const resolvedAlign = align ?? messageSide ?? \"start\";\n\n  return (\n    <MessageBubbleContext.Provider\n      value={{ align: resolvedAlign, animateIn, variant }}\n    >\n      <motion.div\n        data-slot=\"message-bubble\"\n        data-align={resolvedAlign}\n        data-variant={variant}\n        layout={layout}\n        initial={initial ?? false}\n        animate={animate}\n        exit={\n          exit ??\n          (reduce ? { opacity: 0 } : { opacity: 0, y: -3, scale: 0.99 })\n        }\n        transition={transition ?? (reduce ? { duration: 0.12 } : SPRING_LAYOUT)}\n        className={cn(\n          \"group/bubble flex w-full flex-col\",\n          resolvedAlign === \"end\" ? \"items-end\" : \"items-start\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </motion.div>\n    </MessageBubbleContext.Provider>\n  );\n}\n\nfunction bubbleContentClass(\n  variant: MessageBubbleVariant,\n  interactive: boolean,\n) {\n  return cn(\n    \"relative z-0 min-w-9 max-w-[82%] rounded-2xl px-3.5 py-2.5 text-sm leading-6 text-foreground\",\n    \"[&_a]:font-medium [&_a]:underline [&_a]:underline-offset-4 [&_code]:rounded [&_code]:bg-background/60 [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.9em] [&_ol]:my-2 [&_ol]:list-decimal [&_ol]:pl-5 [&_p+p]:mt-2 [&_pre]:my-2 [&_pre]:overflow-x-auto [&_pre]:rounded-xl [&_pre]:bg-background/60 [&_pre]:p-3 [&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-5\",\n    variant === \"solid\" && \"text-background\",\n    variant === \"ghost\" && \"w-full max-w-none rounded-none px-0 py-0\",\n    variant === \"danger\" && \"text-destructive\",\n    interactive &&\n      \"cursor-pointer text-left outline-none transition-[background-color,color,transform] duration-150 hover:brightness-[0.98] focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.99]\",\n  );\n}\n\nfunction bubbleSurfaceClass(\n  variant: MessageBubbleVariant,\n  align: MessageBubbleAlign,\n) {\n  return cn(\n    \"pointer-events-none absolute inset-0 -z-10 rounded-[inherit]\",\n    align === \"end\" ? \"origin-bottom-right\" : \"origin-bottom-left\",\n    variant === \"solid\" && \"bg-foreground\",\n    variant === \"soft\" && \"bg-muted\",\n    variant === \"tint\" && \"bg-muted\",\n    variant === \"outline\" && \"border border-border/70 bg-background\",\n    variant === \"danger\" && \"bg-destructive/10\",\n  );\n}\n\nexport function MessageBubbleContent({\n  render,\n  className,\n  children,\n  ref,\n  ...props\n}: MessageBubbleContentProps) {\n  const reduce = useReducedMotion() ?? false;\n  const { align = \"start\", animateIn, variant } =\n    useContext(MessageBubbleContext);\n  const [layoutVersion, setLayoutVersion] = useState(0);\n  const notifyLayout = useCallback(\n    () => setLayoutVersion((version) => version + 1),\n    [],\n  );\n  const interactive =\n    render?.type === \"button\" || render?.type === \"a\";\n  const classes = cn(bubbleContentClass(variant, interactive), className);\n  const composedChildren = (\n    <>\n      {variant !== \"ghost\" ? (\n        <motion.span\n          aria-hidden=\"true\"\n          layout={reduce ? false : \"size\"}\n          layoutDependency={layoutVersion}\n          initial={\n            animateIn && !reduce\n              ? {\n                  opacity: 0,\n                  scale: 0.92,\n                }\n              : false\n          }\n          animate={{ opacity: 1, scale: 1 }}\n          transition={\n            reduce\n              ? { duration: 0 }\n              : {\n                  opacity: { duration: 0.12, ease: EASE_OUT },\n                  scale: BUBBLE_POP,\n                  layout: SPRING_LAYOUT,\n                }\n          }\n          className={bubbleSurfaceClass(variant, align)}\n        />\n      ) : null}\n      <MessageBubbleLayoutContext.Provider value={notifyLayout}>\n        <motion.div\n          initial={\n            animateIn\n              ? reduce\n                ? { opacity: 0 }\n                : { opacity: 0 }\n              : false\n          }\n          animate={{ opacity: 1 }}\n          transition={\n            reduce ? { duration: 0.12, ease: EASE_OUT } : BUBBLE_CONTENT_REVEAL\n          }\n          className=\"relative\"\n        >\n          {children}\n        </motion.div>\n      </MessageBubbleLayoutContext.Provider>\n    </>\n  );\n\n  if (render) {\n    const child = render as ReactElement<\n      Record<string, unknown> & { className?: string; ref?: Ref<HTMLElement> }\n    >;\n\n    return cloneElement(child, {\n      ...props,\n      ref: mergeRefs(child.props.ref, ref as Ref<HTMLElement> | undefined),\n      className: cn(classes, child.props.className),\n      children: composedChildren,\n      \"data-slot\": \"message-bubble-content\",\n    });\n  }\n\n  return (\n    <div\n      ref={ref}\n      data-slot=\"message-bubble-content\"\n      className={classes}\n      {...props}\n    >\n      {composedChildren}\n    </div>\n  );\n}\n\nexport function MessageBubbleGroup({\n  spacing = \"compact\",\n  className,\n  ...props\n}: MessageBubbleGroupProps) {\n  return (\n    <div\n      data-slot=\"message-bubble-group\"\n      className={cn(\n        \"flex w-full flex-col\",\n        spacing === \"compact\" ? \"gap-1.5\" : \"gap-3\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nconst LINE_CLAMP_CLASS = {\n  2: \"line-clamp-2\",\n  3: \"line-clamp-3\",\n  4: \"line-clamp-4\",\n  5: \"line-clamp-5\",\n  6: \"line-clamp-6\",\n} as const;\n\nexport function MessageBubbleCollapsible({\n  open,\n  defaultOpen = false,\n  onOpenChange,\n  collapsedLines = 4,\n  moreLabel = \"Show more\",\n  lessLabel = \"Show less\",\n  contentClassName,\n  triggerClassName,\n  className,\n  children,\n  ...props\n}: MessageBubbleCollapsibleProps) {\n  const reduce = useReducedMotion() ?? false;\n  const contentId = useId();\n  const notifyLayout = useContext(MessageBubbleLayoutContext);\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const currentOpen = open ?? internalOpen;\n\n  const setOpen = useCallback(\n    (next: boolean) => {\n      notifyLayout();\n      if (open === undefined) setInternalOpen(next);\n      onOpenChange?.(next);\n    },\n    [notifyLayout, onOpenChange, open],\n  );\n\n  return (\n    <div\n      data-slot=\"message-bubble-collapsible\"\n      data-state={currentOpen ? \"open\" : \"closed\"}\n      className={cn(\"w-full\", className)}\n      {...props}\n    >\n      <div\n        id={contentId}\n        className={cn(\n          \"transition-[mask-image] duration-200\",\n          !currentOpen && LINE_CLAMP_CLASS[collapsedLines],\n          !currentOpen &&\n            \"[mask-image:linear-gradient(to_bottom,#000_68%,transparent_100%)]\",\n          contentClassName,\n        )}\n      >\n        {children}\n      </div>\n      <button\n        type=\"button\"\n        aria-expanded={currentOpen}\n        aria-controls={contentId}\n        onClick={() => setOpen(!currentOpen)}\n        className={cn(\n          \"mt-2 inline-flex h-7 items-center gap-1 rounded-full px-2 text-xs font-medium text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\",\n          triggerClassName,\n        )}\n      >\n        <span>{currentOpen ? lessLabel : moreLabel}</span>\n        <motion.span\n          aria-hidden=\"true\"\n          animate={{ rotate: currentOpen ? 180 : 0 }}\n          transition={reduce ? { duration: 0 } : SPRING_SWAP}\n        >\n          <ChevronDown className=\"size-3.5\" />\n        </motion.span>\n      </button>\n    </div>\n  );\n}\n"},{"path":"components/agents/message-context.tsx","type":"util","content":"\"use client\";\n\nimport { createContext } from \"react\";\n\nexport type MessageSide = \"start\" | \"end\";\n\nexport const MessageSideContext = createContext<MessageSide | undefined>(\n  undefined,\n);\n"},{"path":"components/agents/message-scroller.tsx","type":"util","content":"\"use client\";\n\nimport { useReducedMotion } from \"motion/react\";\nimport {\n  type ComponentPropsWithRef,\n  type Ref,\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport {\n  PreviewRail,\n  type PreviewRailItem,\n} from \"@/components/motion/preview-rail\";\nimport { cn } from \"@/lib/utils\";\n\nconst PREVIEW_TITLE_LENGTH = 56;\nconst PREVIEW_DESCRIPTION_LENGTH = 88;\n\nfunction truncateMessageText(text: string, limit: number) {\n  if (text.length <= limit) return text;\n  const excerpt = text.slice(0, limit);\n  const boundary = excerpt.lastIndexOf(\" \");\n  return `${excerpt.slice(0, boundary > limit * 0.65 ? boundary : limit).trim()}…`;\n}\n\nfunction getMessageText(message: HTMLElement) {\n  const surface =\n    message.querySelector<HTMLElement>('[data-slot=\"message-bubble-content\"]') ??\n    message.querySelector<HTMLElement>('[data-slot=\"message-content\"]') ??\n    message;\n  return (surface.textContent ?? \"\").replace(/\\s+/g, \" \").trim();\n}\n\nfunction getMessagePreview(\n  message: HTMLElement,\n  assistantResponse?: HTMLElement,\n) {\n  const text = getMessageText(message);\n  if (!text) {\n    return { label: \"Message\", description: undefined };\n  }\n\n  if (text.length <= PREVIEW_TITLE_LENGTH) {\n    const responseText = assistantResponse\n      ? getMessageText(assistantResponse)\n      : \"\";\n    return {\n      label: text,\n      description: responseText\n        ? truncateMessageText(responseText, PREVIEW_DESCRIPTION_LENGTH)\n        : undefined,\n    };\n  }\n\n  const titleExcerpt = text.slice(0, PREVIEW_TITLE_LENGTH);\n  const titleBoundary = titleExcerpt.lastIndexOf(\" \");\n  const titleEnd =\n    titleBoundary > PREVIEW_TITLE_LENGTH * 0.65\n      ? titleBoundary\n      : PREVIEW_TITLE_LENGTH;\n  const label = `${text.slice(0, titleEnd).trim()}…`;\n  const responseText = assistantResponse\n    ? getMessageText(assistantResponse)\n    : text.slice(titleEnd).trim();\n  return {\n    label,\n    description: responseText\n      ? truncateMessageText(responseText, PREVIEW_DESCRIPTION_LENGTH)\n      : undefined,\n  };\n}\n\nexport interface MessageScrollerProps extends ComponentPropsWithRef<\"div\"> {\n  /** Keep streamed output pinned while the reader remains near the end. */\n  followOutput?: boolean;\n  /** Distance from the end that still counts as following the output. */\n  followThreshold?: number;\n  /** Smoothly follow growing content. */\n  smooth?: boolean;\n  /** Reports when the reader leaves or returns to the live edge. */\n  onFollowChange?: (following: boolean) => void;\n  /** Accessible label for the scrollable transcript. */\n  label?: string;\n  /** Marks the transcript as waiting for more streamed content. */\n  busy?: boolean;\n  /** Adds a compact rail for navigating between rendered Message rows. */\n  navigation?: \"rail\";\n  /** Accessible label for the optional message navigation rail. */\n  navigationLabel?: string;\n  viewportClassName?: string;\n  contentClassName?: string;\n  railClassName?: string;\n  viewportRef?: Ref<HTMLElement>;\n  viewportProps?: Omit<\n    ComponentPropsWithRef<\"section\">,\n    \"children\" | \"className\" | \"ref\"\n  >;\n  contentProps?: Omit<\n    ComponentPropsWithRef<\"div\">,\n    \"children\" | \"className\" | \"ref\"\n  >;\n}\n\nexport function MessageScroller({\n  followOutput = true,\n  followThreshold = 56,\n  smooth = true,\n  onFollowChange,\n  label = \"Conversation\",\n  busy,\n  navigation,\n  navigationLabel = \"Message navigation\",\n  viewportClassName,\n  contentClassName,\n  railClassName,\n  viewportRef: externalViewportRef,\n  viewportProps,\n  contentProps,\n  className,\n  children,\n  ...props\n}: MessageScrollerProps) {\n  const reduce = useReducedMotion() ?? false;\n  const viewportRef = useRef<HTMLElement>(null);\n  const contentRef = useRef<HTMLDivElement>(null);\n  const followingRef = useRef(followOutput);\n  const programmaticScrollRef = useRef(false);\n  const scrollTimerRef = useRef<number | undefined>(undefined);\n  const frameRef = useRef<number | undefined>(undefined);\n  const railFrameRef = useRef<number | undefined>(undefined);\n  const railIdRef = useRef(new WeakMap<HTMLElement, string>());\n  const railIdCounterRef = useRef(0);\n  const railTargetsRef = useRef(new Map<string, HTMLElement>());\n  const [railItems, setRailItems] = useState<PreviewRailItem[]>([]);\n  const [activeRailId, setActiveRailId] = useState(\"\");\n  const [railOverflowing, setRailOverflowing] = useState(false);\n  const {\n    onScroll: onViewportScroll,\n    onWheel: onViewportWheel,\n    onTouchStart: onViewportTouchStart,\n    onKeyDown: onViewportKeyDown,\n    ...restViewportProps\n  } = viewportProps ?? {};\n\n  const setViewportRef = useCallback(\n    (node: HTMLElement | null) => {\n      viewportRef.current = node;\n      if (typeof externalViewportRef === \"function\") {\n        externalViewportRef(node);\n      } else if (externalViewportRef) {\n        externalViewportRef.current = node;\n      }\n    },\n    [externalViewportRef],\n  );\n\n  const setFollowing = useCallback(\n    (next: boolean) => {\n      if (followingRef.current === next) return;\n      followingRef.current = next;\n      onFollowChange?.(next);\n    },\n    [onFollowChange],\n  );\n\n  const updateActiveRailItem = useCallback(() => {\n    if (navigation !== \"rail\") return;\n    const viewport = viewportRef.current;\n    const targets = [...railTargetsRef.current.entries()];\n    if (!viewport || targets.length === 0) return;\n\n    const viewportRect = viewport.getBoundingClientRect();\n    if (viewport.scrollTop <= followThreshold) {\n      const firstId = targets[0]?.[0] ?? \"\";\n      setActiveRailId((current) => (current === firstId ? current : firstId));\n      return;\n    }\n\n    const distanceFromEnd =\n      viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;\n    if (distanceFromEnd <= followThreshold) {\n      const lastId = targets.at(-1)?.[0] ?? \"\";\n      setActiveRailId((current) => (current === lastId ? current : lastId));\n      return;\n    }\n\n    const viewportCenter = viewportRect.top + viewportRect.height / 2;\n    let nearestId = targets[0]?.[0] ?? \"\";\n    let nearestDistance = Number.POSITIVE_INFINITY;\n\n    for (const [id, element] of targets) {\n      const rect = element.getBoundingClientRect();\n      const messageCenter = rect.top + rect.height / 2;\n      const distance = Math.abs(messageCenter - viewportCenter);\n      if (distance < nearestDistance) {\n        nearestDistance = distance;\n        nearestId = id;\n      }\n    }\n\n    setActiveRailId((current) =>\n      current === nearestId ? current : nearestId,\n    );\n  }, [followThreshold, navigation]);\n\n  const syncRailItems = useCallback(() => {\n    if (navigation !== \"rail\") return;\n    const content = contentRef.current;\n    const viewport = viewportRef.current;\n    if (!content || !viewport) return;\n\n    const messages = Array.from(\n      content.querySelectorAll<HTMLElement>('[data-slot=\"message\"]'),\n    );\n    const targets = new Map<string, HTMLElement>();\n    const nextItems = messages.map((message, index) => {\n      let id = railIdRef.current.get(message);\n      if (!id) {\n        railIdCounterRef.current += 1;\n        id = `message-rail-${railIdCounterRef.current}`;\n        railIdRef.current.set(message, id);\n      }\n      targets.set(id, message);\n      const sender = message.dataset.from ?? \"conversation\";\n      const assistantResponse =\n        sender === \"user\"\n          ? messages\n              .slice(index + 1)\n              .find((candidate) => candidate.dataset.from === \"assistant\")\n          : undefined;\n      const preview = getMessagePreview(message, assistantResponse);\n\n      return {\n        id,\n        label: preview.label,\n        description: preview.description,\n        ariaLabel: `Go to ${sender} message ${index + 1} of ${messages.length}`,\n      };\n    });\n\n    railTargetsRef.current = targets;\n    setRailItems((current) => {\n      const unchanged =\n        current.length === nextItems.length &&\n        current.every(\n          (item, index) =>\n            item.id === nextItems[index]?.id &&\n            item.label === nextItems[index]?.label &&\n            item.description === nextItems[index]?.description &&\n            item.ariaLabel === nextItems[index]?.ariaLabel,\n        );\n      return unchanged ? current : nextItems;\n    });\n    setRailOverflowing(\n      viewport.scrollHeight > viewport.clientHeight + 1 && messages.length > 1,\n    );\n  }, [navigation]);\n\n  const scheduleRailSync = useCallback(() => {\n    if (navigation !== \"rail\") return;\n    if (railFrameRef.current) cancelAnimationFrame(railFrameRef.current);\n    railFrameRef.current = requestAnimationFrame(() => {\n      syncRailItems();\n      updateActiveRailItem();\n    });\n  }, [navigation, syncRailItems, updateActiveRailItem]);\n\n  const scrollToEnd = useCallback((behavior: ScrollBehavior) => {\n    const viewport = viewportRef.current;\n    if (!viewport) return;\n\n    programmaticScrollRef.current = true;\n    if (typeof viewport.scrollTo === \"function\") {\n      viewport.scrollTo({ top: viewport.scrollHeight, behavior });\n    } else {\n      viewport.scrollTop = viewport.scrollHeight;\n    }\n    if (scrollTimerRef.current) window.clearTimeout(scrollTimerRef.current);\n    scrollTimerRef.current = window.setTimeout(() => {\n      programmaticScrollRef.current = false;\n    }, behavior === \"smooth\" ? 320 : 0);\n  }, []);\n\n  const handleScroll = useCallback(() => {\n    const viewport = viewportRef.current;\n    if (!viewport || programmaticScrollRef.current) return;\n\n    const distance =\n      viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;\n    setFollowing(distance <= followThreshold);\n    updateActiveRailItem();\n  }, [followThreshold, setFollowing, updateActiveRailItem]);\n\n  const leaveLiveEdge = useCallback(() => {\n    programmaticScrollRef.current = false;\n  }, []);\n\n  useLayoutEffect(() => {\n    followingRef.current = followOutput;\n    if (!followOutput) return;\n\n    frameRef.current = requestAnimationFrame(() => scrollToEnd(\"auto\"));\n    return () => {\n      if (frameRef.current) cancelAnimationFrame(frameRef.current);\n    };\n  }, [followOutput, scrollToEnd]);\n\n  useEffect(() => {\n    const content = contentRef.current;\n    if (!content || typeof ResizeObserver === \"undefined\") return;\n\n    const observer = new ResizeObserver(() => {\n      scheduleRailSync();\n      if (!followOutput || !followingRef.current) return;\n      scrollToEnd(reduce || !smooth ? \"auto\" : \"smooth\");\n    });\n    observer.observe(content);\n\n    return () => observer.disconnect();\n  }, [followOutput, reduce, scheduleRailSync, scrollToEnd, smooth]);\n\n  useEffect(() => {\n    if (navigation !== \"rail\") {\n      railTargetsRef.current.clear();\n      setRailItems([]);\n      setRailOverflowing(false);\n      return;\n    }\n\n    const content = contentRef.current;\n    const viewport = viewportRef.current;\n    if (!content || !viewport) return;\n\n    scheduleRailSync();\n    const mutationObserver =\n      typeof MutationObserver === \"undefined\"\n        ? null\n        : new MutationObserver(scheduleRailSync);\n    mutationObserver?.observe(content, {\n      childList: true,\n      characterData: true,\n      subtree: true,\n    });\n\n    const resizeObserver =\n      typeof ResizeObserver === \"undefined\"\n        ? null\n        : new ResizeObserver(scheduleRailSync);\n    resizeObserver?.observe(content);\n    resizeObserver?.observe(viewport);\n\n    return () => {\n      mutationObserver?.disconnect();\n      resizeObserver?.disconnect();\n    };\n  }, [navigation, scheduleRailSync]);\n\n  useEffect(\n    () => () => {\n      if (scrollTimerRef.current) window.clearTimeout(scrollTimerRef.current);\n      if (frameRef.current) cancelAnimationFrame(frameRef.current);\n      if (railFrameRef.current) cancelAnimationFrame(railFrameRef.current);\n    },\n    [],\n  );\n\n  const scrollToRailItem = useCallback(\n    (item: PreviewRailItem) => {\n      const viewport = viewportRef.current;\n      const target = railTargetsRef.current.get(item.id);\n      if (!viewport || !target) return;\n\n      const lastItem = railItems.at(-1)?.id === item.id;\n      setActiveRailId(item.id);\n      if (lastItem) {\n        setFollowing(true);\n        scrollToEnd(reduce || !smooth ? \"auto\" : \"smooth\");\n        return;\n      }\n\n      setFollowing(false);\n      programmaticScrollRef.current = true;\n      const viewportRect = viewport.getBoundingClientRect();\n      const targetRect = target.getBoundingClientRect();\n      const top =\n        viewport.scrollTop +\n        targetRect.top -\n        viewportRect.top -\n        (viewport.clientHeight - targetRect.height) / 2;\n      const behavior = reduce || !smooth ? \"auto\" : \"smooth\";\n\n      if (typeof viewport.scrollTo === \"function\") {\n        viewport.scrollTo({ top, behavior });\n      } else {\n        viewport.scrollTop = top;\n      }\n      if (scrollTimerRef.current) window.clearTimeout(scrollTimerRef.current);\n      scrollTimerRef.current = window.setTimeout(() => {\n        programmaticScrollRef.current = false;\n      }, behavior === \"smooth\" ? 320 : 0);\n    },\n    [railItems, reduce, scrollToEnd, setFollowing, smooth],\n  );\n\n  const viewport = (\n    <section\n      ref={setViewportRef}\n      aria-label={label}\n      {...restViewportProps}\n      onScroll={(event) => {\n        handleScroll();\n        onViewportScroll?.(event);\n      }}\n      onWheel={(event) => {\n        leaveLiveEdge();\n        onViewportWheel?.(event);\n      }}\n      onTouchStart={(event) => {\n        leaveLiveEdge();\n        onViewportTouchStart?.(event);\n      }}\n      onKeyDown={(event) => {\n        if ([\"ArrowUp\", \"PageUp\", \"Home\"].includes(event.key)) {\n          leaveLiveEdge();\n        }\n        onViewportKeyDown?.(event);\n      }}\n      className={cn(\n        \"h-full overflow-y-auto overscroll-contain outline-none [overflow-anchor:none] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring\",\n        navigation === \"rail\"\n          ? \"[-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n          : \"[scrollbar-gutter:stable]\",\n        viewportClassName,\n        navigation === \"rail\" && railOverflowing && \"pr-10\",\n      )}\n    >\n      <div\n        ref={contentRef}\n        role=\"log\"\n        aria-live=\"polite\"\n        aria-relevant=\"additions text\"\n        aria-busy={busy}\n        className={contentClassName}\n        {...contentProps}\n      >\n        {children}\n      </div>\n    </section>\n  );\n\n  return (\n    <div\n      data-slot=\"message-scroller\"\n      className={cn(\"min-h-0\", className)}\n      {...props}\n    >\n      {navigation === \"rail\" ? (\n        <PreviewRail\n          items={railOverflowing ? railItems : []}\n          label={navigationLabel}\n          activeId={activeRailId}\n          onItemSelect={scrollToRailItem}\n          previewSide=\"before\"\n          highlightActive\n          itemSize={14}\n          className=\"h-full min-h-0 overflow-hidden\"\n          previewContainerClassName=\"right-8 left-3\"\n          previewClassName=\"mr-1 w-64 max-w-full [&_[data-slot=preview-rail-card]]:h-20 [&_[data-slot=preview-rail-card]]:overflow-hidden [&_[data-slot=preview-rail-card]]:p-3 [&_[data-slot=preview-rail-title]]:line-clamp-1 [&_[data-slot=preview-rail-title]]:text-xs [&_[data-slot=preview-rail-title]]:leading-4 [&_[data-slot=preview-rail-description]]:line-clamp-2 [&_[data-slot=preview-rail-description]]:text-xs [&_[data-slot=preview-rail-description]]:leading-4\"\n          railClassName={cn(\n            \"absolute inset-y-3 right-1 w-7 content-center py-1 [&_[data-slot=preview-rail-item]]:w-7 [&_[data-slot=preview-rail-item]]:justify-end [&_[data-slot=preview-rail-tick]]:h-px [&_[data-slot=preview-rail-tick]]:w-4 [&_[data-slot=preview-rail-tick]]:origin-right\",\n            railOverflowing\n              ? \"pointer-events-auto opacity-100\"\n              : \"pointer-events-none opacity-0\",\n            railClassName,\n          )}\n        >\n          {viewport}\n        </PreviewRail>\n      ) : (\n        viewport\n      )}\n    </div>\n  );\n}\n"},{"path":"lib/ease.ts","type":"util","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":"util","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/preview-rail.tsx","type":"util","content":"\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  type MouseEvent,\n  type PointerEvent,\n  type ReactNode,\n  useCallback,\n  useId,\n  useRef,\n  useState,\n} from \"react\";\nimport { EASE_OUT, SPRING_LAYOUT } from \"@/lib/ease\";\nimport { useDismiss } from \"@/lib/hooks/use-dismiss\";\nimport { useHoverGesture } from \"@/lib/hooks/use-hover-gesture\";\nimport { useTapGesture } from \"@/lib/hooks/use-tap-gesture\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface PreviewRailItem {\n  id: string;\n  label: string;\n  ariaLabel?: string;\n  description?: ReactNode;\n  href?: string;\n  target?: \"_blank\" | \"_self\" | \"_parent\" | \"_top\";\n  rel?: string;\n}\n\nexport interface PreviewRailProps {\n  items: PreviewRailItem[];\n  label?: string;\n  orientation?: \"vertical\" | \"horizontal\";\n  activeId?: string;\n  defaultActiveId?: string;\n  onActiveChange?: (id: string) => void;\n  onItemSelect?: (item: PreviewRailItem) => void;\n  renderPreview?: (item: PreviewRailItem) => ReactNode;\n  showPreview?: boolean;\n  previewSide?: \"before\" | \"after\";\n  highlightActive?: boolean;\n  itemSize?: number;\n  children?: ReactNode;\n  className?: string;\n  railClassName?: string;\n  previewContainerClassName?: string;\n  previewClassName?: string;\n}\n\nfunction DefaultPreview({ item }: { item: PreviewRailItem }) {\n  return (\n    <div\n      data-slot=\"preview-rail-card\"\n      className=\"rounded-2xl border border-border bg-card p-4 shadow-sm\"\n    >\n      <p\n        data-slot=\"preview-rail-title\"\n        className=\"font-medium text-card-foreground\"\n      >\n        {item.label}\n      </p>\n      {item.description ? (\n        <div\n          data-slot=\"preview-rail-description\"\n          className=\"mt-1 text-sm leading-6 text-muted-foreground\"\n        >\n          {item.description}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n\nexport function PreviewRail({\n  items,\n  label = \"Section navigation\",\n  orientation = \"vertical\",\n  activeId,\n  defaultActiveId,\n  onActiveChange,\n  onItemSelect,\n  renderPreview,\n  showPreview = true,\n  previewSide = \"after\",\n  highlightActive = false,\n  itemSize = 24,\n  children,\n  className,\n  railClassName,\n  previewContainerClassName,\n  previewClassName,\n}: PreviewRailProps) {\n  const uid = useId();\n  const reduce = useReducedMotion();\n  const rootRef = useRef<HTMLDivElement>(null);\n  const [internalActiveId, setInternalActiveId] = useState(\n    defaultActiveId ?? items[0]?.id ?? \"\",\n  );\n  const [hoveredId, setHoveredId] = useState<string | null>(null);\n  // A finger cannot hover, so a tap lights the tick instead. Kept apart from\n  // the hovered one: they end in different ways, and a stray mouse move must\n  // not clear a tick the keyboard or a tap chose.\n  const [pinnedId, setPinnedId] = useState<string | null>(null);\n  const [focusedId, setFocusedId] = useState<string | null>(null);\n  // A click carries no pointerType, so the pointerdown before it is what says\n  // whether the activation was a tap. Keyboard activation has none at all.\n  const tap = useTapGesture<boolean>();\n  const hover = useHoverGesture();\n\n  const clearPinned = useCallback(() => setPinnedId(null), []);\n\n  // The next tap outside the rail stands in for the pointer leaving it. The\n  // card is a preview, so that tap passes through to whatever it landed on.\n  useDismiss(pinnedId !== null, clearPinned, rootRef);\n\n  const requestedActiveId = activeId ?? internalActiveId;\n  const selectedId = items.some((item) => item.id === requestedActiveId)\n    ? requestedActiveId\n    : (items[0]?.id ?? \"\");\n  const displayedId = hoveredId ?? pinnedId ?? focusedId ?? \"\";\n  const highlightedId = displayedId || (highlightActive ? selectedId : \"\");\n  const displayedIndex = items.findIndex((item) => item.id === highlightedId);\n  const rowTemplate = items.length\n    ? `repeat(${items.length}, ${itemSize}px)`\n    : undefined;\n  const isHorizontal = orientation === \"horizontal\";\n\n  const selectItem = (id: string) => {\n    if (activeId === undefined) setInternalActiveId(id);\n    onActiveChange?.(id);\n  };\n\n  return (\n    <motion.div\n      layoutRoot\n      ref={rootRef}\n      onBlur={(event) => {\n        // Both tick sources leave with the focus: a tap does not always land\n        // focus, but when it does, tabbing away must not strand the card.\n        if (!event.currentTarget.contains(event.relatedTarget)) {\n          setFocusedId(null);\n          setPinnedId(null);\n        }\n      }}\n      className={cn(\n        \"isolate relative flex w-full overflow-visible\",\n        isHorizontal\n          ? \"min-h-64 flex-col items-center justify-center\"\n          : \"min-h-80\",\n        className,\n      )}\n    >\n      <nav\n        aria-label={label}\n        onPointerLeave={(event) => {\n          // A touch pointer leaves on lift, which would clear the tick the tap\n          // just chose — that one is cleared by the outside tap instead.\n          if (hover.leave(event)) setHoveredId(null);\n        }}\n        style={\n          isHorizontal\n            ? { gridTemplateColumns: rowTemplate }\n            : { gridTemplateRows: rowTemplate }\n        }\n        className={cn(\n          \"relative z-10 grid shrink-0\",\n          isHorizontal\n            ? \"h-12 w-fit max-w-full self-center justify-center\"\n            : \"w-12 content-center\",\n          railClassName,\n        )}\n      >\n        {items.map((item, index) => {\n          const selected = item.id === selectedId;\n          const highlighted = item.id === highlightedId;\n          const distance =\n            displayedIndex < 0 ? Number.POSITIVE_INFINITY : Math.abs(index - displayedIndex);\n          const scale = highlighted\n            ? 1\n            : distance === 1\n              ? 0.68\n              : distance === 2\n                ? 0.44\n                : 0.25;\n\n          const itemContent = (\n            <>\n              <motion.span\n                data-slot=\"preview-rail-tick\"\n                aria-hidden=\"true\"\n                animate={isHorizontal ? { scaleY: scale } : { scaleX: scale }}\n                transition={reduce ? { duration: 0 } : SPRING_LAYOUT}\n                className={cn(\n                  \"block bg-current\",\n                  isHorizontal\n                    ? \"h-12 w-0.5 origin-bottom\"\n                    : \"h-0.5 w-12 origin-left\",\n                  highlighted ? \"text-foreground\" : undefined,\n                )}\n              />\n            </>\n          );\n\n          const sharedClassName = cn(\n            \"relative flex text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n            isHorizontal\n              ? \"h-12 w-6 items-end justify-center\"\n              : \"h-6 w-12 items-center\",\n          );\n          const sharedStyle = isHorizontal\n            ? { width: itemSize }\n            : { height: itemSize };\n          const handlePointerEnter = (event: PointerEvent<HTMLElement>) => {\n            if (hover.enter(event)) setHoveredId(item.id);\n          };\n          const handlePointerDown = (event: PointerEvent<HTMLElement>) => {\n            tap.start(event, pinnedId === item.id);\n            setFocusedId(null);\n          };\n          // A gesture the platform takes away sends no click, and a key press\n          // starts an activation that never had a pointer behind it: either\n          // one leaves a record the next click would read as a tap of its own.\n          const dropGesture = () => tap.drop();\n          const handleFocus = (currentTarget: HTMLElement) => {\n            if (currentTarget.matches(\":focus-visible\")) {\n              setFocusedId(item.id);\n            }\n          };\n          const handleSelect = (event: MouseEvent<HTMLElement>) => {\n            const gesture = tap.take();\n            const tapped =\n              gesture !== null && gesture.pointerType !== \"mouse\";\n\n            if (tapped) {\n              // A link would otherwise show its preview and leave the page in\n              // the same tap, so the card is never read: the first tap lights\n              // the tick, the second follows the link.\n              if (item.href && !gesture.state) {\n                event.preventDefault();\n                setPinnedId(item.id);\n                return;\n              }\n              setPinnedId(item.id);\n            }\n\n            selectItem(item.id);\n            onItemSelect?.(item);\n          };\n\n          return item.href ? (\n            <a\n              key={item.id}\n              data-slot=\"preview-rail-item\"\n              href={item.href}\n              target={item.target}\n              rel={\n                item.rel ??\n                (item.target === \"_blank\" ? \"noreferrer noopener\" : undefined)\n              }\n              aria-label={item.ariaLabel ?? item.label}\n              aria-current={selected ? \"page\" : undefined}\n              onPointerEnter={handlePointerEnter}\n              onPointerDown={handlePointerDown}\n              onPointerCancel={dropGesture}\n              onKeyDown={dropGesture}\n              onFocus={(event) => handleFocus(event.currentTarget)}\n              onClick={handleSelect}\n              style={sharedStyle}\n              className={sharedClassName}\n            >\n              {itemContent}\n            </a>\n          ) : (\n            <button\n              key={item.id}\n              data-slot=\"preview-rail-item\"\n              type=\"button\"\n              aria-label={item.ariaLabel ?? item.label}\n              aria-current={selected ? \"location\" : undefined}\n              onPointerEnter={handlePointerEnter}\n              onPointerDown={handlePointerDown}\n              onPointerCancel={dropGesture}\n              onKeyDown={dropGesture}\n              onFocus={(event) => handleFocus(event.currentTarget)}\n              onClick={handleSelect}\n              style={sharedStyle}\n              className={sharedClassName}\n            >\n              {itemContent}\n            </button>\n          );\n        })}\n      </nav>\n\n      {showPreview ? (\n        <div\n          aria-hidden=\"true\"\n          style={\n            isHorizontal\n              ? { gridTemplateColumns: rowTemplate }\n              : { gridTemplateRows: rowTemplate }\n          }\n          className={cn(\n            \"pointer-events-none absolute z-50 grid\",\n            isHorizontal\n              ? \"top-1/2 left-1/2 h-5 w-fit max-w-full -translate-x-1/2 -translate-y-1/2 justify-center\"\n              : previewSide === \"before\"\n                ? \"inset-y-0 right-16 left-4 content-center\"\n                : \"inset-y-0 right-4 left-16 content-center\",\n            previewContainerClassName,\n          )}\n        >\n          {items.map((item) => (\n            <div\n              key={item.id}\n              style={\n                isHorizontal ? { width: itemSize } : { height: itemSize }\n              }\n              className={cn(\n                \"relative flex items-center\",\n                isHorizontal ? \"justify-center\" : undefined,\n              )}\n            >\n              {item.id === displayedId ? (\n                <div\n                  className={cn(\n                    isHorizontal\n                      ? \"absolute bottom-12 left-1/2 w-72 -translate-x-1/2\"\n                      : cn(\n                          \"w-full max-w-sm\",\n                          previewSide === \"before\" && \"ml-auto\",\n                        ),\n                    previewClassName,\n                  )}\n                >\n                  <motion.div\n                    layoutId={`preview-rail-card-${uid}`}\n                    transition={reduce ? { duration: 0 } : SPRING_LAYOUT}\n                  >\n                    <AnimatePresence mode=\"wait\" initial={false}>\n                      <motion.div\n                        key={item.id}\n                        initial={\n                          reduce\n                            ? { opacity: 0 }\n                            : { opacity: 0, y: 4, filter: \"blur(6px)\" }\n                        }\n                        animate={\n                          reduce\n                            ? { opacity: 1 }\n                            : { opacity: 1, y: 0, filter: \"blur(0px)\" }\n                        }\n                        exit={\n                          reduce\n                            ? { opacity: 0 }\n                            : {\n                                opacity: 0,\n                                y: -2,\n                                filter: \"blur(4px)\",\n                                transition: {\n                                  duration: 0.12,\n                                  ease: EASE_OUT,\n                                },\n                              }\n                        }\n                        transition={{\n                          duration: reduce ? 0 : 0.18,\n                          ease: EASE_OUT,\n                        }}\n                      >\n                        {renderPreview ? (\n                          renderPreview(item)\n                        ) : (\n                          <DefaultPreview item={item} />\n                        )}\n                      </motion.div>\n                    </AnimatePresence>\n                  </motion.div>\n                </div>\n              ) : null}\n            </div>\n          ))}\n        </div>\n      ) : null}\n\n      {children ? (\n        <div className=\"min-h-0 min-w-0 flex-1\">{children}</div>\n      ) : null}\n    </motion.div>\n  );\n}\n"},{"path":"lib/hooks/use-dismiss.ts","type":"util","content":"\"use client\";\n\nimport { type RefObject, useEffect } from \"react\";\n\n/**\n * What the dismissing gesture does to the control it landed on.\n *\n * `\"pass-through\"` is the platform norm (native popover light-dismiss): the\n * tap closes the overlay *and* activates whatever was under it. Use\n * `\"consume\"` where the open overlay sits over or beside controls that would\n * be costly to trigger by accident — the dismissal then swallows the\n * activation too, so the gesture only closes.\n */\nexport type DismissBehavior = \"pass-through\" | \"consume\";\n\nexport interface DismissOptions {\n  /** Default `\"pass-through\"`. */\n  behavior?: DismissBehavior;\n  /** Dismiss on Escape as well. Default true. */\n  escape?: boolean;\n  /** Return true for an outside target that should *not* dismiss. Must be stable. */\n  ignore?: (target: Element) => boolean;\n}\n\n/**\n * What every currently open dismiss scope counts as inside itself. A consumed\n * dismissal reads this to tell a stray gesture from one that belongs to an\n * overlay in front of it: overlays have no shared z-order to consult, but the\n * one the gesture landed in has said as much by registering it.\n */\nconst openScopes = new Set<(target: Element) => boolean>();\n\nfunction claimedByAnotherScope(\n  self: (target: Element) => boolean,\n  target: Element,\n) {\n  for (const scope of openScopes) {\n    if (scope !== self && scope(target)) return true;\n  }\n  return false;\n}\n\n// preventDefault on pointerdown does not suppress the click that follows, so\n// consuming a gesture means swallowing that click itself. The swallower\n// deliberately outlives the effect that installed it — the dismissal it\n// belongs to has already unmounted or re-rendered by the time the click lands.\n// It releases on that click, or on the next gesture if the pointer is dragged\n// away and no click ever arrives, so it can never eat a later one. A keydown\n// releases it too: a gesture that ends with neither a click nor a cancel would\n// otherwise leave it armed, and the click Enter synthesizes on some focused\n// control is not the one this dismissal was owed.\nfunction consumeActivation(source: Event) {\n  const swallow = (event: MouseEvent) => {\n    event.preventDefault();\n    event.stopPropagation();\n    release();\n  };\n  const restart = (event: Event) => {\n    if (event !== source) release();\n  };\n  const release = () => {\n    window.removeEventListener(\"click\", swallow, true);\n    window.removeEventListener(\"pointerdown\", restart, true);\n    window.removeEventListener(\"pointercancel\", restart, true);\n    window.removeEventListener(\"keydown\", release, true);\n  };\n  window.addEventListener(\"click\", swallow, true);\n  window.addEventListener(\"pointerdown\", restart, true);\n  window.addEventListener(\"pointercancel\", restart, true);\n  window.addEventListener(\"keydown\", release, true);\n}\n\n/**\n * Close an open overlay on Escape or a pointerdown outside `ref`. Pass `null`\n * for `ref` when what counts as inside isn't one element, and say so with\n * `ignore` instead.\n *\n * The pointerdown listener is capture-phase: a bubble-phase one is blinded by\n * any handler in between that stops propagation, and an overlay cannot know\n * what it is layered over. `onDismiss` and `ignore` must be stable (wrap in\n * useCallback) so the listeners aren't re-bound every render while open.\n */\nexport function useDismiss(\n  open: boolean,\n  onDismiss: () => void,\n  ref: RefObject<HTMLElement | SVGElement | null> | null,\n  {\n    behavior = \"pass-through\",\n    escape: dismissOnEscape = true,\n    ignore,\n  }: DismissOptions = {},\n) {\n  useEffect(() => {\n    if (!open) return;\n    const inside = (target: Element) =>\n      Boolean(ref?.current?.contains(target)) || Boolean(ignore?.(target));\n    const onKey = (event: KeyboardEvent) => {\n      if (dismissOnEscape && event.key === \"Escape\") onDismiss();\n    };\n    const onPointer = (event: PointerEvent) => {\n      const target = event.target as Element | null;\n      if (!target || inside(target)) return;\n      // Outside this overlay, but inside one that is also open: the gesture is\n      // that overlay's to answer, and swallowing its click from behind would\n      // cost the user the control they actually aimed at.\n      if (behavior === \"consume\" && !claimedByAnotherScope(inside, target)) {\n        consumeActivation(event);\n      }\n      onDismiss();\n    };\n    openScopes.add(inside);\n    window.addEventListener(\"keydown\", onKey);\n    window.addEventListener(\"pointerdown\", onPointer, true);\n    return () => {\n      openScopes.delete(inside);\n      window.removeEventListener(\"keydown\", onKey);\n      window.removeEventListener(\"pointerdown\", onPointer, true);\n    };\n  }, [open, onDismiss, ref, behavior, dismissOnEscape, ignore]);\n}\n"},{"path":"lib/hooks/use-hover-gesture.ts","type":"util","content":"\"use client\";\n\nimport { useMemo, useRef } from \"react\";\nimport { isHoveringPointer } from \"@/lib/touch\";\n\ninterface BoundaryEvent {\n  pointerId: number;\n  pointerType: string;\n  buttons: number;\n}\n\nexport interface HoverGesture {\n  /** True when this enter starts a hover: the pointer arrived resting, not pressing. */\n  enter: (event: BoundaryEvent) => boolean;\n  /** True when this leave ends a hover that entered as one. */\n  leave: (event: BoundaryEvent) => boolean;\n}\n\n/**\n * Pairs a surface's enter with its leave, per pointer.\n *\n * `isHoveringPointer` answers the question the *enter* asks — is this pointer\n * resting on the surface or pressing it — and both boundary cases go wrong if\n * the leave is asked the same question again:\n *\n * - A pen with no hover never rests. It arrives in contact, taps, and the spec\n *   then requires its boundary events after `pointerup`, so the leave carries\n *   `buttons: 0` and reads as a mouse gliding off. Hover teardown then undid\n *   the tap — the panel the pen had just opened closed under it.\n * - A mouse pressed on the surface and dragged off leaves with `buttons: 1`.\n *   Skipping teardown there strands the surface open: the release happens\n *   outside, and no second leave ever comes.\n *\n * So the state a hover holds is released by the pointer that took it, whatever\n * the buttons say at the boundary, and a pointer that arrived in contact never\n * took it in the first place. Contact is the exception tracked here, not\n * hover: a leave from a pointer this surface never saw enter — mounted under\n * the cursor, say — still counts, since the alternative is state with no way\n * out.\n */\nexport function useHoverGesture(): HoverGesture {\n  const contact = useRef(new Set<number>());\n\n  return useMemo(\n    () => ({\n      enter: (event) => {\n        if (isHoveringPointer(event)) {\n          contact.current.delete(event.pointerId);\n          return true;\n        }\n        contact.current.add(event.pointerId);\n        return false;\n      },\n      leave: (event) => {\n        const arrivedInContact = contact.current.delete(event.pointerId);\n        return !arrivedInContact && event.pointerType !== \"touch\";\n      },\n    }),\n    [],\n  );\n}\n"},{"path":"lib/hooks/use-tap-gesture.ts","type":"util","content":"\"use client\";\n\nimport { useMemo, useRef } from \"react\";\n\n/** What a pointerdown recorded, read back by the click that ends its gesture. */\nexport interface TapRecord<S> {\n  /** Which input started the gesture. */\n  pointerType: string;\n  /** What the surface was showing when it started. */\n  state: S;\n}\n\nexport interface TapGesture<S> {\n  /** Record the gesture a pointerdown starts, with the state it starts in. */\n  start: (event: { pointerType: string }, state: S) => void;\n  /** Read the record and clear it. `null` when no pointer is behind this click. */\n  take: () => TapRecord<S> | null;\n  /** Drop the record: this gesture will never spend it on a click. */\n  drop: () => void;\n}\n\n/**\n * The pointer gesture behind a click, recorded where the click cannot report\n * it. A `click` carries no `pointerType` in the engines that matter, so the\n * `pointerdown` before it is the only thing that says which input activated\n * the control — and whether one did at all, since keyboard activation\n * synthesizes a click with no pointer behind it.\n *\n * State goes in with the record because a click reports that no better: a\n * browser that focuses a control on contact can open the very panel the tap\n * was meant to open, and reading \"is it open\" at click time then undoes it.\n * What the gesture started against is what it acts on.\n *\n * The record is spent by one click and dropped by everything else, because a\n * record that outlives its gesture is worse than none:\n *\n * - A scroll or an OS gesture takes the touch away — `pointercancel`, no click\n *   ever — and the finger would sit in the record until some later click.\n * - That later click is often `Enter` on a keyboard, which arrives with no\n *   pointerdown of its own and would inherit the abandoned finger. A keydown\n *   is the start of a keyboard activation and never part of a tap, so it drops\n *   the record too.\n *\n * Both ends have to be wired by the surface: `drop` on `onPointerCancel` and\n * on `onKeyDown`.\n */\nexport function useTapGesture<S>(): TapGesture<S> {\n  const record = useRef<TapRecord<S> | null>(null);\n\n  return useMemo(\n    () => ({\n      start: (event, state) => {\n        record.current = { pointerType: event.pointerType, state };\n      },\n      take: () => {\n        const spent = record.current;\n        record.current = null;\n        return spent;\n      },\n      drop: () => {\n        record.current = null;\n      },\n    }),\n    [],\n  );\n}\n"},{"path":"lib/touch.ts","type":"util","content":"// Shared touch primitives. iOS and iPadOS run their own gestures on top of the\n// page — the long-press selection callout and the selection it drags in with\n// it — and they win: once the platform claims a touch it cancels ours\n// mid-gesture, so a press-and-hold or a drag simply dies. Surfaces that own\n// their gesture have to opt out.\n//\n// What the two classes below cover, precisely:\n// - `-webkit-touch-callout: none` stops iOS's long-press callout. WebKit-only:\n//   it is not a property other engines have, so it is inert everywhere else.\n// - `user-select: none` stops the long-press selection on every engine,\n//   Android included, and stops a drag from painting a selection under the\n//   cursor. It is inherited, so it reaches every descendant — which is why the\n//   two classes differ only in whether they apply it unconditionally.\n// What neither covers:\n// - Chrome for Android's long-press menu on a link or an image. No CSS\n//   suppresses it; a gesture surface that wraps one needs its own\n//   `onContextMenu` with `preventDefault()`.\n// - The native drag of an `<img>` or `<a>` descendant. `-webkit-user-drag` is\n//   not inherited and plain divs and buttons are not drag sources, so setting\n//   it on the surface does nothing — the child itself needs `draggable={false}`.\n\n/**\n * Classes for a surface that *is* the control: a thumb, a drum, a stage, a\n * handle, a hold button. Selection is suppressed on every input, because a\n * drag that highlights the control's own label is wrong on a mouse too.\n * Compose with `touch-none` when the surface also owns the scroll axis — leave\n * it off when the page must still scroll from there.\n */\nexport const TOUCH_GESTURE_CLASS = \"select-none [-webkit-touch-callout:none]\";\n\n/**\n * The same opt-out for a gesture surface that wraps content the consumer owns:\n * a scroller, a context-menu trigger, a sheet header, a list row. Selection is\n * suppressed only where the platform runs its own press gestures — a coarse\n * pointer — so a mouse user can still select and copy that content. If the\n * gesture itself would paint a selection under the cursor, add `select-none`\n * for the duration of the gesture rather than reaching for\n * `TOUCH_GESTURE_CLASS`.\n *\n * `pointer: coarse` describes the *primary* pointer and nothing else, so a\n * hybrid machine reads it wrong in both directions: a tablet with a mouse\n * plugged in keeps touch as primary and loses mouse selection, and a laptop\n * with a touchscreen keeps the mouse as primary and leaves selection live\n * under a finger. No media query can answer per interaction — the query is\n * about the device, and the question is about the gesture in progress. The\n * default stays here because it is right on the machines that are one thing or\n * the other, and losing a selection is a nuisance; where the miss costs a\n * *gesture* instead, the surface pairs it with `holdSelection` on the press.\n */\nexport const TOUCH_GESTURE_CONTENT_CLASS =\n  \"[-webkit-touch-callout:none] pointer-coarse:select-none\";\n\n/**\n * Suppress selection on `element` for as long as a gesture is running on it,\n * whatever the primary pointer of the machine happens to be. Returns the\n * release. Inline, so it wins over the class above and is gone again the\n * moment the gesture ends.\n *\n * For the press gestures a native selection would otherwise steal — a\n * long-press that opens a menu. Elsewhere prefer the classes: a surface that\n * takes selection away for the whole session is a surface whose text nobody\n * can copy.\n */\nexport function holdSelection(element: HTMLElement) {\n  element.style.setProperty(\"user-select\", \"none\");\n  element.style.setProperty(\"-webkit-user-select\", \"none\");\n  return () => {\n    element.style.removeProperty(\"user-select\");\n    element.style.removeProperty(\"-webkit-user-select\");\n  };\n}\n\n/**\n * Pointer capture, best effort. WebKit throws `NotFoundError` when the pointer\n * is already gone by the time the handler runs — routine on iOS, where the\n * system can claim the touch first — and an uncaught throw takes the rest of\n * the handler, the gesture included, down with it. Touch pointers carry\n * implicit capture anyway, so losing it is never fatal.\n */\nexport function capturePointer(element: Element, pointerId: number) {\n  try {\n    element.setPointerCapture(pointerId);\n  } catch {\n    // Pointer is no longer active — implicit capture still applies on touch.\n  }\n}\n\n/** Release a capture taken with `capturePointer`, ignoring a stale pointer. */\nexport function releasePointer(element: Element, pointerId: number) {\n  try {\n    if (element.hasPointerCapture(pointerId)) {\n      element.releasePointerCapture(pointerId);\n    }\n  } catch {\n    // Capture was already dropped by the browser.\n  }\n}\n\n/**\n * Whether this event came from a pointer that is *hovering*: not a touch, and\n * not currently pressed. Which input the user is holding right now is not\n * something a device capability can answer — a touchscreen laptop hovers and\n * taps, and iPadOS reports a fine hovering pointer for a finger — so both\n * paths stay live and each handler branches on the event it was given.\n *\n * A pen resting on the glass is making contact, not hovering: `buttons` is the\n * tell, and it sends a pen tap down the same route a finger takes.\n *\n * This answers what an *enter* asks. A leave is the other half of a pair and\n * has to be read against the enter that started it — `useHoverGesture` in\n * `lib/hooks/use-hover-gesture` does that, and hover surfaces should use it\n * rather than asking this question twice.\n */\nexport const isHoveringPointer = (event: {\n  pointerType: string;\n  buttons: number;\n}) => event.pointerType !== \"touch\" && event.buttons === 0;\n"},{"path":"components/previews/agents/message.preview.tsx","type":"preview","content":"\"use client\";\n\nimport { ChatPreview } from \"@/components/previews/agents/chat-preview\";\n\nexport function MessagePreview() {\n  return (\n    <ChatPreview\n      showAvatars\n      showMetadata\n      assistantVariant=\"ghost\"\n      placeholder=\"Ask a follow-up…\"\n    />\n  );\n}\n"},{"path":"components/previews/agents/chat-preview.tsx","type":"util","content":"\"use client\";\n\nimport { Bot, User } from \"lucide-react\";\nimport { useReducedMotion } from \"motion/react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n  Message,\n  MessageAvatar,\n  MessageContent,\n  MessageFooter,\n  MessageGroup,\n  MessageHeader,\n  MessageTyping,\n} from \"@/components/agents/message\";\nimport {\n  MessageBubble,\n  MessageBubbleContent,\n  type MessageBubbleVariant,\n} from \"@/components/agents/message-bubble\";\nimport { MessageScroller } from \"@/components/agents/message-scroller\";\nimport { PromptInput } from \"@/components/agents/prompt-input\";\nimport { StreamingResponse } from \"@/components/agents/streaming-response\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface ChatPreviewMessage {\n  id: string;\n  from: \"user\" | \"assistant\";\n  content: string;\n}\n\ninterface RenderedChatMessage extends ChatPreviewMessage {\n  animateIn: boolean;\n  streaming?: boolean;\n}\n\nexport interface ChatPreviewProps {\n  initialMessages?: ChatPreviewMessage[];\n  reply?: string | ((prompt: string) => string);\n  showAvatars?: boolean;\n  showMetadata?: boolean;\n  showRail?: boolean;\n  assistantVariant?: MessageBubbleVariant;\n  userVariant?: MessageBubbleVariant;\n  placeholder?: string;\n  className?: string;\n  viewportClassName?: string;\n}\n\nconst DEFAULT_MESSAGES: ChatPreviewMessage[] = [\n  {\n    id: \"welcome-question\",\n    from: \"user\",\n    content: \"What should we include in the first release?\",\n  },\n  {\n    id: \"welcome-answer\",\n    from: \"assistant\",\n    content: \"Start with the smallest workflow that still feels complete.\",\n  },\n];\n\nconst DEFAULT_REPLY =\n  \"I’d include composing a prompt, following a streamed answer, recovering from an error, and returning to the latest response without losing the reader’s place.\";\nconst CHARACTERS_PER_SECOND = 96;\nconst STREAM_DELAY = 140;\n\nexport function ChatPreview({\n  initialMessages = DEFAULT_MESSAGES,\n  reply = DEFAULT_REPLY,\n  showAvatars = false,\n  showMetadata = false,\n  showRail = false,\n  assistantVariant = \"soft\",\n  userVariant = \"solid\",\n  placeholder = \"Send a message…\",\n  className,\n  viewportClassName,\n}: ChatPreviewProps) {\n  const reduce = useReducedMotion() ?? false;\n  const nextId = useRef(0);\n  const replyTimer = useRef<number | undefined>(undefined);\n  const [messages, setMessages] = useState<RenderedChatMessage[]>(() =>\n    initialMessages.map((message) => ({ ...message, animateIn: false })),\n  );\n  const [pending, setPending] = useState(false);\n  const [activeReply, setActiveReply] = useState<{\n    id: string;\n    content: string;\n  } | null>(null);\n  const loading = pending || activeReply !== null;\n\n  useEffect(() => {\n    if (!activeReply) return;\n\n    if (reduce) {\n      setMessages((current) =>\n        current.map((message) =>\n          message.id === activeReply.id\n            ? { ...message, content: activeReply.content, streaming: false }\n            : message,\n        ),\n      );\n      setActiveReply(null);\n      return;\n    }\n\n    const startedAt = performance.now() + STREAM_DELAY;\n    let frame = 0;\n    const stream = (now: number) => {\n      const cursor = Math.min(\n        activeReply.content.length,\n        Math.floor(\n          (Math.max(0, now - startedAt) / 1000) * CHARACTERS_PER_SECOND,\n        ),\n      );\n      const content = activeReply.content.slice(0, cursor);\n      setMessages((current) =>\n        current.map((message) =>\n          message.id === activeReply.id && message.content !== content\n            ? { ...message, content }\n            : message,\n        ),\n      );\n\n      if (cursor < activeReply.content.length) {\n        frame = requestAnimationFrame(stream);\n      } else {\n        setMessages((current) =>\n          current.map((message) =>\n            message.id === activeReply.id\n              ? { ...message, streaming: false }\n              : message,\n          ),\n        );\n        setActiveReply(null);\n      }\n    };\n\n    frame = requestAnimationFrame(stream);\n    return () => cancelAnimationFrame(frame);\n  }, [activeReply, reduce]);\n\n  useEffect(\n    () => () => {\n      if (replyTimer.current) window.clearTimeout(replyTimer.current);\n    },\n    [],\n  );\n\n  const submit = useCallback(\n    (prompt: string) => {\n      if (loading) return;\n\n      const run = nextId.current++;\n      const userId = `sent-user-${run}`;\n      const assistantId = `sent-assistant-${run}`;\n      const response = typeof reply === \"function\" ? reply(prompt) : reply;\n\n      setMessages((current) => [\n        ...current,\n        { id: userId, from: \"user\", content: prompt, animateIn: true },\n      ]);\n      setPending(true);\n\n      replyTimer.current = window.setTimeout(() => {\n        setMessages((current) => [\n          ...current,\n          {\n            id: assistantId,\n            from: \"assistant\",\n            content: \"\",\n            animateIn: true,\n            streaming: true,\n          },\n        ]);\n        setPending(false);\n        setActiveReply({ id: assistantId, content: response });\n      }, reduce ? 0 : 420);\n    },\n    [loading, reduce, reply],\n  );\n\n  const stop = useCallback(() => {\n    if (replyTimer.current) window.clearTimeout(replyTimer.current);\n    replyTimer.current = undefined;\n    setPending(false);\n    setMessages((current) =>\n      current.map((message) =>\n        message.streaming ? { ...message, streaming: false } : message,\n      ),\n    );\n    setActiveReply(null);\n  }, []);\n\n  return (\n    <div\n      className={cn(\n        \"flex h-[440px] w-full max-w-xl flex-col overflow-hidden rounded-2xl border border-border/70 bg-background\",\n        className,\n      )}\n    >\n      <MessageScroller\n        busy={loading}\n        navigation={showRail ? \"rail\" : undefined}\n        className=\"min-h-0 flex-1\"\n        viewportClassName={cn(\"px-3 py-4\", viewportClassName)}\n        contentClassName=\"min-h-full\"\n      >\n        <MessageGroup spacing=\"default\">\n          {messages.map((message) => (\n            <Message\n              key={message.id}\n              id={message.id}\n              from={message.from}\n              animateIn={message.from === \"user\" && message.animateIn}\n            >\n              {showAvatars ? (\n                <MessageAvatar>\n                  {message.from === \"user\" ? <User /> : <Bot />}\n                </MessageAvatar>\n              ) : null}\n              <MessageContent>\n                {showMetadata ? (\n                  <MessageHeader>\n                    <span className=\"font-medium text-foreground/70\">\n                      {message.from === \"user\" ? \"You\" : \"Assistant\"}\n                    </span>\n                    <span>{message.streaming ? \"Responding\" : \"Now\"}</span>\n                  </MessageHeader>\n                ) : null}\n                <MessageBubble\n                  variant={\n                    message.from === \"user\" ? userVariant : assistantVariant\n                  }\n                  animateIn={false}\n                >\n                  <MessageBubbleContent>\n                    {message.from === \"assistant\" ? (\n                      <StreamingResponse\n                        status={message.streaming ? \"streaming\" : \"complete\"}\n                        showActions={false}\n                        announce={false}\n                      >\n                        {message.content || <MessageTyping />}\n                      </StreamingResponse>\n                    ) : (\n                      message.content\n                    )}\n                  </MessageBubbleContent>\n                </MessageBubble>\n                {showMetadata && message.from === \"user\" ? (\n                  <MessageFooter>Sent</MessageFooter>\n                ) : null}\n              </MessageContent>\n            </Message>\n          ))}\n          {pending ? (\n            <Message id=\"pending-assistant\" from=\"assistant\">\n              {showAvatars ? (\n                <MessageAvatar>\n                  <Bot />\n                </MessageAvatar>\n              ) : null}\n              <MessageContent>\n                <MessageTyping label=\"Preparing response\" />\n              </MessageContent>\n            </Message>\n          ) : null}\n        </MessageGroup>\n      </MessageScroller>\n\n      <div className=\"shrink-0 border-t border-border/60 p-2\">\n        <PromptInput\n          minRows={1}\n          maxRows={1}\n          loading={loading}\n          onSubmit={submit}\n          onStop={stop}\n          placeholder={placeholder}\n          className=\"border-0 bg-muted shadow-none focus-within:border-transparent\"\n        />\n      </div>\n    </div>\n  );\n}\n"},{"path":"components/agents/prompt-input.tsx","type":"util","content":"\"use client\";\n\nimport { ArrowUp, Plus, Square } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  type FormEvent,\n  type KeyboardEvent,\n  type ReactNode,\n  type TextareaHTMLAttributes,\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { Button } from \"@/components/motion/button\";\nimport {\n  MorphPopover,\n  MorphPopoverContent,\n  MorphPopoverTrigger,\n} from \"@/components/motion/popover-morph\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n} from \"@/components/motion/select\";\nimport { SPRING_SWAP } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface PromptModel {\n  value: string;\n  label: ReactNode;\n  icon?: ReactNode;\n  disabled?: boolean;\n}\n\nexport interface PromptAction {\n  value: string;\n  label: ReactNode;\n  description?: ReactNode;\n  icon?: ReactNode;\n  disabled?: boolean;\n}\n\nexport interface PromptInputProps extends Omit<\n  TextareaHTMLAttributes<HTMLTextAreaElement>,\n  \"value\" | \"defaultValue\" | \"onChange\" | \"onSubmit\" | \"children\"\n> {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  models?: PromptModel[];\n  model?: string;\n  defaultModel?: string;\n  onModelChange?: (model: string) => void;\n  actions?: PromptAction[];\n  onAction?: (action: string) => void;\n  onSubmit?: (value: string, model?: string) => void | Promise<void>;\n  loading?: boolean;\n  onStop?: () => void;\n  minRows?: number;\n  maxRows?: number;\n  leadingAction?: ReactNode;\n  className?: string;\n}\n\nexport function PromptInput({\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  models = [],\n  model,\n  defaultModel,\n  onModelChange,\n  actions = [],\n  onAction,\n  onSubmit,\n  loading = false,\n  onStop,\n  minRows = 2,\n  maxRows = 8,\n  leadingAction,\n  className,\n  disabled,\n  placeholder = \"Ask the agent to do something…\",\n  \"aria-label\": ariaLabel = \"Prompt\",\n  onKeyDown,\n  ...textareaProps\n}: PromptInputProps) {\n  const reduce = useReducedMotion() ?? false;\n  const textareaRef = useRef<HTMLTextAreaElement>(null);\n  const measurementRef = useRef<HTMLDivElement>(null);\n  const [internalValue, setInternalValue] = useState(defaultValue);\n  const [internalModel, setInternalModel] = useState(\n    defaultModel ?? models[0]?.value,\n  );\n  const [actionsOpen, setActionsOpen] = useState(false);\n  const currentValue = value ?? internalValue;\n  const currentModelValue = model ?? internalModel;\n  const currentModel = models.find(\n    (option) => option.value === currentModelValue,\n  );\n  const canSubmit = Boolean(currentValue.trim()) && !disabled && !loading;\n\n  const resizeTextarea = useCallback(() => {\n    const textarea = textareaRef.current;\n    const measurement = measurementRef.current;\n    if (!textarea || !measurement || textarea.value !== currentValue) return;\n\n    const lineHeight = 24;\n    const nextHeight = Math.min(\n      Math.max(measurement.scrollHeight, minRows * lineHeight),\n      maxRows * lineHeight,\n    );\n    const height = `${nextHeight}px`;\n    if (textarea.style.height !== height) textarea.style.height = height;\n  }, [currentValue, maxRows, minRows]);\n\n  useLayoutEffect(() => {\n    resizeTextarea();\n  }, [resizeTextarea]);\n\n  useEffect(() => {\n    const textarea = textareaRef.current;\n    if (!textarea || typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(resizeTextarea);\n    observer.observe(textarea);\n    return () => observer.disconnect();\n  }, [resizeTextarea]);\n\n  const setValue = (next: string) => {\n    if (value === undefined) setInternalValue(next);\n    onValueChange?.(next);\n  };\n\n  const setModel = (next: string) => {\n    if (model === undefined) setInternalModel(next);\n    onModelChange?.(next);\n  };\n\n  const submit = (event?: FormEvent) => {\n    event?.preventDefault();\n    const prompt = currentValue.trim();\n    if (!prompt || disabled || loading) return;\n\n    onSubmit?.(prompt, currentModelValue);\n    if (value === undefined) setInternalValue(\"\");\n    textareaRef.current?.focus({ preventScroll: true });\n  };\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {\n    onKeyDown?.(event);\n    if (\n      event.defaultPrevented ||\n      event.key !== \"Enter\" ||\n      event.shiftKey ||\n      event.nativeEvent.isComposing\n    ) {\n      return;\n    }\n    event.preventDefault();\n    submit();\n  };\n\n  return (\n    <form\n      onSubmit={submit}\n      className={cn(\n        \"relative w-full rounded-2xl border border-border/80 bg-background p-2 transition-colors focus-within:border-foreground/25\",\n        disabled && \"opacity-60\",\n        className,\n      )}\n    >\n      <div\n        ref={measurementRef}\n        aria-hidden=\"true\"\n        className=\"pointer-events-none invisible absolute inset-x-2 top-0 whitespace-pre-wrap px-2 text-sm leading-6 [overflow-wrap:break-word]\"\n      >\n        {`${currentValue}\\u200b`}\n      </div>\n      <textarea\n        ref={textareaRef}\n        value={currentValue}\n        disabled={disabled}\n        placeholder={placeholder}\n        aria-label={ariaLabel}\n        rows={minRows}\n        {...textareaProps}\n        onChange={(event) => setValue(event.target.value)}\n        onKeyDown={handleKeyDown}\n        className=\"scrollbar-hide block w-full resize-none overflow-y-auto bg-transparent px-2 pt-1.5 text-sm leading-6 text-foreground outline-none placeholder:text-muted-foreground/55\"\n      />\n\n      <div className=\"mt-1 flex min-h-8 items-center gap-1\">\n        {actions.length ? (\n          <MorphPopover open={actionsOpen} onOpenChange={setActionsOpen}>\n            <MorphPopoverTrigger>\n              <Button\n                type=\"button\"\n                variant=\"ghost\"\n                size=\"icon\"\n                disabled={disabled || loading}\n                aria-label=\"Add to prompt\"\n                className=\"size-8 rounded-full\"\n              >\n                <motion.span\n                  aria-hidden=\"true\"\n                  animate={{ rotate: actionsOpen ? 45 : 0 }}\n                  transition={reduce ? { duration: 0 } : SPRING_SWAP}\n                >\n                  <Plus className=\"size-4\" />\n                </motion.span>\n              </Button>\n            </MorphPopoverTrigger>\n\n            <MorphPopoverContent\n              side=\"top\"\n              align=\"start\"\n              sideOffset={8}\n              radius={12}\n              className=\"w-56 p-1.5\"\n            >\n              {actions.map((action) => (\n                <button\n                  key={action.value}\n                  type=\"button\"\n                  disabled={action.disabled}\n                  onClick={() => {\n                    onAction?.(action.value);\n                    setActionsOpen(false);\n                  }}\n                  className=\"flex w-full items-start gap-2.5 rounded-lg px-2.5 py-2 text-left outline-none transition-colors hover:bg-muted focus-visible:bg-muted disabled:pointer-events-none disabled:opacity-50\"\n                >\n                  {action.icon ? (\n                    <span className=\"mt-0.5 grid size-5 shrink-0 place-items-center text-muted-foreground [&_svg]:size-4\">\n                      {action.icon}\n                    </span>\n                  ) : null}\n                  <span className=\"min-w-0\">\n                    <span className=\"block text-sm text-foreground\">\n                      {action.label}\n                    </span>\n                    {action.description ? (\n                      <span className=\"mt-0.5 block text-xs leading-4 text-muted-foreground\">\n                        {action.description}\n                      </span>\n                    ) : null}\n                  </span>\n                </button>\n              ))}\n            </MorphPopoverContent>\n          </MorphPopover>\n        ) : null}\n        {leadingAction}\n        {models.length ? (\n          <Select\n            value={currentModelValue}\n            onValueChange={setModel}\n            disabled={disabled || loading}\n            className=\"min-w-0\"\n          >\n            <SelectTrigger className=\"h-8 w-auto max-w-52 rounded-xl border-0 bg-transparent px-2 py-0 text-xs hover:bg-muted focus-visible:ring-2\">\n              <span className=\"flex min-w-0 items-center gap-1.5\">\n                {currentModel?.icon ? (\n                  <span className=\"grid size-4 shrink-0 place-items-center text-muted-foreground [&_svg]:size-3.5\">\n                    {currentModel.icon}\n                  </span>\n                ) : null}\n                <span className=\"truncate text-muted-foreground\">\n                  {currentModel?.label ?? \"Choose model\"}\n                </span>\n              </span>\n            </SelectTrigger>\n            <SelectContent className=\"right-auto w-52 shadow-none\">\n              {models.map((option) => (\n                <SelectItem\n                  key={option.value}\n                  value={option.value}\n                  disabled={option.disabled}\n                  className=\"py-2\"\n                >\n                  <span className=\"flex min-w-0 items-center gap-2\">\n                    {option.icon ? (\n                      <span className=\"grid size-5 shrink-0 place-items-center text-muted-foreground [&_svg]:size-4\">\n                        {option.icon}\n                      </span>\n                    ) : null}\n                    <span className=\"min-w-0 truncate text-sm text-foreground\">\n                      {option.label}\n                    </span>\n                  </span>\n                </SelectItem>\n              ))}\n            </SelectContent>\n          </Select>\n        ) : null}\n\n        <Button\n          type={loading ? \"button\" : \"submit\"}\n          size=\"icon\"\n          disabled={loading ? !onStop : !canSubmit}\n          aria-label={loading ? \"Stop generating\" : \"Send prompt\"}\n          onClick={loading ? onStop : undefined}\n          className=\"ml-auto size-8 rounded-full\"\n        >\n          <AnimatePresence initial={false} mode=\"popLayout\">\n            <motion.span\n              key={loading ? \"stop\" : \"send\"}\n              initial={reduce ? { opacity: 1 } : { opacity: 0, y: 3, scale: 0.8 }}\n              animate={{ opacity: 1, y: 0, scale: 1 }}\n              exit={reduce ? { opacity: 0 } : { opacity: 0, y: -3, scale: 0.8 }}\n              transition={reduce ? { duration: 0 } : SPRING_SWAP}\n              className=\"grid place-items-center\"\n            >\n              {loading ? (\n                <Square className=\"size-3 fill-current\" />\n              ) : (\n                <ArrowUp className=\"size-4\" />\n              )}\n            </motion.span>\n          </AnimatePresence>\n        </Button>\n      </div>\n    </form>\n  );\n}\n"},{"path":"components/agents/streaming-response.tsx","type":"util","content":"\"use client\";\n\nimport {\n  Check,\n  ChevronDown,\n  Copy,\n  RotateCcw,\n  ThumbsDown,\n  ThumbsUp,\n} from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n} from \"react\";\nimport {\n  type CitationItem,\n  CitationList,\n  CitationStack,\n} from \"@/components/agents/citations\";\nimport { AgentDisclosure } from \"@/components/agents/agent-disclosure\";\nimport { EASE_OUT, SPRING_PRESS, SPRING_SWAP } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport type StreamingResponseStatus = \"streaming\" | \"complete\" | \"error\";\nexport type StreamingResponseFeedback = \"up\" | \"down\" | null;\n\nexport interface StreamingResponseProps {\n  /** Rendered response content. Pass plain text or the output of a Markdown renderer. */\n  children: ReactNode;\n  status?: StreamingResponseStatus;\n  /** Plain-text value copied by the built-in copy action. */\n  copyText?: string;\n  /** Overrides the built-in clipboard action. */\n  onCopy?: () => void | Promise<void>;\n  onRetry?: () => void;\n  /** Optional sources shown as a compact footer disclosure after streaming. */\n  sources?: CitationItem[];\n  sourcesOpen?: boolean;\n  defaultSourcesOpen?: boolean;\n  onSourcesOpenChange?: (open: boolean) => void;\n  sourceIdPrefix?: string;\n  feedback?: StreamingResponseFeedback;\n  defaultFeedback?: StreamingResponseFeedback;\n  onFeedbackChange?: (feedback: StreamingResponseFeedback) => void;\n  /** Set false when a surrounding conversation log announces streamed text. */\n  announce?: boolean;\n  /** Hides the built-in completion actions without changing response status. */\n  showActions?: boolean;\n  className?: string;\n  contentClassName?: string;\n  actionsClassName?: string;\n}\n\nfunction ResponseAction({\n  label,\n  active = false,\n  onClick,\n  children,\n}: {\n  label: string;\n  active?: boolean;\n  onClick: () => void;\n  children: ReactNode;\n}) {\n  const reduce = useReducedMotion() ?? false;\n\n  return (\n    <motion.button\n      type=\"button\"\n      aria-label={label}\n      title={label}\n      aria-pressed={label === \"Helpful\" || label === \"Not helpful\" ? active : undefined}\n      onClick={onClick}\n      whileTap={reduce ? undefined : { scale: 0.9 }}\n      transition={SPRING_PRESS}\n      className={cn(\n        \"grid size-7 place-items-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\",\n        active && \"bg-muted text-foreground\",\n      )}\n    >\n      {children}\n    </motion.button>\n  );\n}\n\nexport function StreamingResponse({\n  children,\n  status = \"streaming\",\n  copyText,\n  onCopy,\n  onRetry,\n  sources = [],\n  sourcesOpen,\n  defaultSourcesOpen = false,\n  onSourcesOpenChange,\n  sourceIdPrefix,\n  feedback,\n  defaultFeedback = null,\n  onFeedbackChange,\n  announce = true,\n  showActions = true,\n  className,\n  contentClassName,\n  actionsClassName,\n}: StreamingResponseProps) {\n  const reduce = useReducedMotion() ?? false;\n  const baseId = useId();\n  const [copied, setCopied] = useState(false);\n  const [internalFeedback, setInternalFeedback] =\n    useState<StreamingResponseFeedback>(defaultFeedback);\n  const [internalSourcesOpen, setInternalSourcesOpen] =\n    useState(defaultSourcesOpen);\n  const copyTimer = useRef<number | undefined>(undefined);\n  const currentFeedback = feedback ?? internalFeedback;\n  const currentSourcesOpen = sourcesOpen ?? internalSourcesOpen;\n  const streaming = status === \"streaming\";\n  const complete = status === \"complete\";\n  const canCopy = Boolean(copyText || onCopy);\n  const hasSources = sources.length > 0;\n  const shouldShowActions =\n    showActions && !streaming && (canCopy || onRetry || complete || hasSources);\n  const sourcesContentId = `${baseId}-sources`;\n  const resolvedSourcePrefix =\n    sourceIdPrefix ?? `response-source-${baseId.replace(/:/g, \"\")}`;\n\n  useEffect(\n    () => () => {\n      if (copyTimer.current) window.clearTimeout(copyTimer.current);\n    },\n    [],\n  );\n\n  const handleCopy = useCallback(async () => {\n    if (onCopy) await onCopy();\n    else if (copyText) await navigator.clipboard?.writeText(copyText);\n\n    setCopied(true);\n    if (copyTimer.current) window.clearTimeout(copyTimer.current);\n    copyTimer.current = window.setTimeout(() => setCopied(false), 1600);\n  }, [copyText, onCopy]);\n\n  const setFeedback = (next: Exclude<StreamingResponseFeedback, null>) => {\n    const value = currentFeedback === next ? null : next;\n    if (feedback === undefined) setInternalFeedback(value);\n    onFeedbackChange?.(value);\n  };\n\n  const setSourcesOpen = useCallback(\n    (next: boolean) => {\n      if (sourcesOpen === undefined) setInternalSourcesOpen(next);\n      onSourcesOpenChange?.(next);\n    },\n    [onSourcesOpenChange, sourcesOpen],\n  );\n\n  return (\n    <div\n      data-state={status}\n      aria-busy={streaming}\n      className={cn(\"w-full\", className)}\n    >\n      <div\n        aria-live={announce ? \"polite\" : \"off\"}\n        className={cn(\n          \"text-sm leading-6 text-foreground/90 [&_a]:font-medium [&_a]:underline [&_a]:underline-offset-4 [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.9em] [&_ol]:my-3 [&_ol]:list-decimal [&_ol]:space-y-1 [&_ol]:pl-5 [&_p+p]:mt-3 [&_pre]:my-3 [&_pre]:overflow-x-auto [&_pre]:rounded-xl [&_pre]:border [&_pre]:border-border [&_pre]:bg-muted/45 [&_pre]:p-3 [&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_ul]:my-3 [&_ul]:list-disc [&_ul]:space-y-1 [&_ul]:pl-5\",\n          contentClassName,\n        )}\n      >\n        {children}\n      </div>\n\n      <AnimatePresence initial={false}>\n        {shouldShowActions ? (\n          <motion.div\n            initial={reduce ? { opacity: 0 } : { opacity: 0, y: 4 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0 }}\n            transition={{ duration: reduce ? 0.12 : 0.22, ease: EASE_OUT }}\n            className=\"mt-3\"\n          >\n            <div className={cn(\"flex items-center gap-0.5\", actionsClassName)}>\n              {canCopy ? (\n                <ResponseAction\n                  label={copied ? \"Copied\" : \"Copy response\"}\n                  onClick={handleCopy}\n                >\n                  {copied ? (\n                    <Check className=\"size-3.5\" />\n                  ) : (\n                    <Copy className=\"size-3.5\" />\n                  )}\n                </ResponseAction>\n              ) : null}\n              {onRetry ? (\n                <ResponseAction label=\"Retry response\" onClick={onRetry}>\n                  <RotateCcw className=\"size-3.5\" />\n                </ResponseAction>\n              ) : null}\n              {complete ? (\n                <>\n                  <ResponseAction\n                    label=\"Helpful\"\n                    active={currentFeedback === \"up\"}\n                    onClick={() => setFeedback(\"up\")}\n                  >\n                    <ThumbsUp className=\"size-3.5\" />\n                  </ResponseAction>\n                  <ResponseAction\n                    label=\"Not helpful\"\n                    active={currentFeedback === \"down\"}\n                    onClick={() => setFeedback(\"down\")}\n                  >\n                    <ThumbsDown className=\"size-3.5\" />\n                  </ResponseAction>\n                </>\n              ) : null}\n              {hasSources ? (\n                <button\n                  type=\"button\"\n                  aria-expanded={currentSourcesOpen}\n                  aria-controls={sourcesContentId}\n                  onClick={() => setSourcesOpen(!currentSourcesOpen)}\n                  className=\"group ml-1 inline-flex min-h-7 items-center gap-2 rounded-md px-1.5 text-xs text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n                >\n                  <CitationStack citations={sources} />\n                  <span className=\"tabular-nums\">\n                    {sources.length} {sources.length === 1 ? \"source\" : \"sources\"}\n                  </span>\n                  <motion.span\n                    aria-hidden=\"true\"\n                    animate={{ rotate: currentSourcesOpen ? 180 : 0 }}\n                    transition={reduce ? { duration: 0 } : SPRING_SWAP}\n                    className=\"text-muted-foreground/50 group-hover:text-muted-foreground\"\n                  >\n                    <ChevronDown className=\"size-3\" />\n                  </motion.span>\n                </button>\n              ) : null}\n            </div>\n\n            {hasSources ? (\n              <AgentDisclosure\n                id={sourcesContentId}\n                open={currentSourcesOpen}\n              >\n                <CitationList\n                  citations={sources}\n                  idPrefix={resolvedSourcePrefix}\n                  className=\"mt-2 rounded-xl bg-muted p-2\"\n                />\n              </AgentDisclosure>\n            ) : null}\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n}\n"},{"path":"components/motion/button/index.tsx","type":"util","content":"export type {\n  ButtonLinkProps,\n  ButtonProps,\n  ButtonSize,\n  ButtonVariant,\n} from \"./base\";\nexport { Button, ButtonLink } from \"./base\";\nexport type { MagneticButtonProps } from \"./magnetic\";\nexport { MagneticButton } from \"./magnetic\";\nexport type { MetallicButtonProps } from \"./metallic\";\nexport { MetallicButton } from \"./metallic\";\nexport type { ButtonState, StatefulButtonProps } from \"./stateful\";\nexport { StatefulButton } from \"./stateful\";\n"},{"path":"components/motion/popover-morph.tsx","type":"util","content":"\"use client\";\n\nimport {\n  AnimatePresence,\n  motion,\n  animate,\n  useMotionValue,\n  usePresence,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  cloneElement,\n  createContext,\n  isValidElement,\n  type ReactElement,\n  type ReactNode,\n  type Ref,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { usePopoverPortalPosition } from \"@/components/motion/popover-position\";\nimport { EASE_OUT, SPRING_PANEL } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\ntype Side = \"top\" | \"bottom\";\ntype Align = \"start\" | \"end\";\n\ntype MorphContextValue = {\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  toggle: () => void;\n  triggerId: string;\n  contentId: string;\n  /** The element the panel measures against — see `registerTrigger`. */\n  triggerRef: React.MutableRefObject<HTMLElement | null>;\n  registerTrigger: (node: HTMLElement | null) => void;\n  contentRef: React.MutableRefObject<HTMLDivElement | null>;\n};\n\nconst MorphContext = createContext<MorphContextValue | null>(null);\n\nfunction useMorphContext(component: string) {\n  const ctx = useContext(MorphContext);\n  if (!ctx) throw new Error(`${component} must be used within <MorphPopover>`);\n  return ctx;\n}\n\nexport interface MorphPopoverProps {\n  children: ReactNode;\n  /** Controlled open state. */\n  open?: boolean;\n  /** Uncontrolled initial open state. */\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  className?: string;\n}\n\n/**\n * A popover whose panel morphs open from the trigger corner: it's laid out at\n * full size but clipped to the corner nearest the trigger, then unclips as one\n * piece. Closes on outside pointer / Escape. Controlled or uncontrolled.\n */\nexport function MorphPopover({\n  children,\n  open: controlledOpen,\n  defaultOpen = false,\n  onOpenChange,\n  className,\n}: MorphPopoverProps) {\n  const baseId = useId();\n  const [root, setRoot] = useState<HTMLDivElement | null>(null);\n  const [trigger, setTrigger] = useState<HTMLElement | null>(null);\n  const contentRef = useRef<HTMLDivElement | null>(null);\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const controlled = controlledOpen !== undefined;\n  const open = controlled ? controlledOpen : internalOpen;\n\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (!controlled) setInternalOpen(next);\n      onOpenChange?.(next);\n    },\n    [controlled, onOpenChange],\n  );\n  const toggle = useCallback(() => setOpen(!open), [setOpen, open]);\n\n  // A trigger normally registers itself through MorphPopoverTrigger. It can't\n  // when something else already clones the element — a Tooltip wrapping the\n  // button, say — and an unregistered trigger leaves the panel with nothing to\n  // measure against, so it renders permanently invisible. The root boxes the\n  // trigger exactly (the content portals out of it), so it stands in until a\n  // real trigger registers, and stands in again if that one unmounts. Both are\n  // state, so a trigger arriving while the panel is open re-anchors it.\n  const anchorRef = useMemo<React.MutableRefObject<HTMLElement | null>>(\n    () => ({ current: trigger ?? root }),\n    [root, trigger],\n  );\n\n  // The panel is a `role=\"dialog\"` and goes inert the moment it closes, so\n  // focus cannot be left sitting inside it: a dismissal hands it back to the\n  // trigger, the way the ARIA dialog pattern asks. A pointer dismissal takes\n  // the focus onward itself when it lands on something focusable — this only\n  // catches the case where it would otherwise be stranded. When no trigger has\n  // registered, the root anchor stands in only if it can actually hold focus;\n  // there is nowhere better than where the keyboard already is, so leave it.\n  const close = useCallback(() => {\n    setOpen(false);\n    const focused = document.activeElement;\n    const inPanel =\n      focused instanceof HTMLElement && contentRef.current?.contains(focused);\n    if (!inPanel) return;\n    const restore = trigger ?? (root && root.tabIndex >= 0 ? root : null);\n    restore?.focus();\n  }, [root, setOpen, trigger]);\n\n  useEffect(() => {\n    if (!open) return;\n    const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && close();\n    const onPointer = (e: PointerEvent) => {\n      const target = e.target as Node;\n      if (\n        root &&\n        !root.contains(target) &&\n        !contentRef.current?.contains(target)\n      )\n        close();\n    };\n    window.addEventListener(\"keydown\", onKey);\n    window.addEventListener(\"pointerdown\", onPointer);\n    return () => {\n      window.removeEventListener(\"keydown\", onKey);\n      window.removeEventListener(\"pointerdown\", onPointer);\n    };\n  }, [open, root, close]);\n\n  const ctx = useMemo<MorphContextValue>(\n    () => ({\n      open,\n      setOpen,\n      toggle,\n      triggerId: `${baseId}-trigger`,\n      contentId: `${baseId}-content`,\n      triggerRef: anchorRef,\n      registerTrigger: setTrigger,\n      contentRef,\n    }),\n    [open, setOpen, toggle, baseId, anchorRef],\n  );\n\n  return (\n    <MorphContext.Provider value={ctx}>\n      <div ref={setRoot} className={cn(\"relative inline-flex\", className)}>\n        {children}\n      </div>\n    </MorphContext.Provider>\n  );\n}\n\nexport interface MorphPopoverTriggerProps {\n  children: ReactElement;\n}\n\nfunction mergeRefs<T>(...refs: Array<Ref<T> | undefined>) {\n  return (node: T | null) => {\n    for (const ref of refs) {\n      if (typeof ref === \"function\") ref(node);\n      else if (ref && typeof ref === \"object\")\n        (ref as React.MutableRefObject<T | null>).current = node;\n    }\n  };\n}\n\n/** Wraps a single element, toggling the popover on click. */\nexport function MorphPopoverTrigger({ children }: MorphPopoverTriggerProps) {\n  const ctx = useMorphContext(\"MorphPopoverTrigger\");\n  const child = children as ReactElement<Record<string, unknown>>;\n  const childOnClick = child?.props?.onClick as\n    | ((e: unknown) => void)\n    | undefined;\n  const childRef = (child?.props as { ref?: Ref<HTMLElement> } | undefined)\n    ?.ref;\n  // Register once per actual ref change, not once per open-state render.\n  const mergedRef = useMemo(\n    () => mergeRefs(childRef, ctx.registerTrigger),\n    [childRef, ctx.registerTrigger],\n  );\n  if (!isValidElement(children)) return children;\n\n  return cloneElement(child, {\n    id: ctx.triggerId,\n    ref: mergedRef,\n    onClick: (e: unknown) => {\n      childOnClick?.(e);\n      ctx.toggle();\n    },\n    \"aria-haspopup\": \"dialog\",\n    \"aria-expanded\": ctx.open,\n    \"aria-controls\": ctx.open ? ctx.contentId : undefined,\n  });\n}\n\nconst originFor = (side: Side, align: Align) =>\n  `${side === \"bottom\" ? \"top\" : \"bottom\"} ${align === \"end\" ? \"right\" : \"left\"}`;\n\n// A clip that hides everything but the corner nearest the trigger, so the\n// panel appears to grow out of it. inset(top right bottom left).\nfunction clipAt(side: Side, align: Align, radius: number, inset: number) {\n  const top = side === \"bottom\" ? \"0%\" : `${inset}%`;\n  const bottom = side === \"bottom\" ? `${inset}%` : \"0%\";\n  const right = align === \"end\" ? \"0%\" : `${inset}%`;\n  const left = align === \"end\" ? `${inset}%` : \"0%\";\n  return `inset(${top} ${right} ${bottom} ${left} round ${radius}px)`;\n}\n\n// Preserve the original spring character on the wrapper, but tween the complex\n// clip-path so it cannot snap when the spring resolves its final distance.\nconst MORPH_CLIP_TRANSITION = { duration: 0.32, ease: EASE_OUT } as const;\n\nexport interface MorphPopoverContentProps {\n  children: ReactNode;\n  side?: Side;\n  align?: Align;\n  /** Gap between trigger and panel, in px. Default 8. */\n  sideOffset?: number;\n  /** Panel corner radius, in px. Default 16. */\n  radius?: number;\n  className?: string;\n}\n\nexport function MorphPopoverContent(props: MorphPopoverContentProps) {\n  const ctx = useMorphContext(\"MorphPopoverContent\");\n  const [portalReady, setPortalReady] = useState(false);\n  useEffect(() => setPortalReady(true), []);\n  if (!portalReady) return null;\n  return createPortal(\n    <AnimatePresence>\n      {ctx.open && <MorphPopoverSurface {...props} />}\n    </AnimatePresence>,\n    document.body,\n  );\n}\n\n// Measurement belongs to the mounted portal session: reopening must not start\n// an entrance at the previous session's coordinates before measuring this one.\nfunction MorphPopoverSurface({\n  children,\n  side = \"bottom\",\n  align = \"end\",\n  sideOffset = 8,\n  radius = 16,\n  className,\n}: MorphPopoverContentProps) {\n  const ctx = useMorphContext(\"MorphPopoverContent\");\n  const reduce = useReducedMotion() ?? false;\n  const [isPresent, safeToRemove] = usePresence();\n  const layout = usePopoverPortalPosition(\n    ctx.triggerRef,\n    ctx.contentRef,\n    isPresent,\n  );\n\n  const left = layout\n    ? align === \"end\"\n      ? layout.trigger.left + layout.trigger.width - layout.content.width\n      : layout.trigger.left\n    : 0;\n  const top = layout\n    ? side === \"bottom\"\n      ? layout.trigger.top + layout.trigger.height + sideOffset\n      : layout.trigger.top - layout.content.height - sideOffset\n    : 0;\n\n  // Both directions travel between the exact same hidden/show states. Exit\n  // targets \"hidden\" directly instead of introducing separate choreography.\n  const wrap = reduce\n    ? undefined\n    : {\n        hidden: { scale: 0.96, transition: SPRING_PANEL },\n        show: { scale: 1, transition: SPRING_PANEL },\n      };\n  const clip = reduce\n    ? undefined\n    : {\n        hidden: {\n          clipPath: clipAt(side, align, radius, 92),\n          transition: MORPH_CLIP_TRANSITION,\n        },\n        show: {\n          clipPath: clipAt(side, align, radius, 0),\n          transition: MORPH_CLIP_TRANSITION,\n        },\n      };\n  // Animate the value directly so opacity stays in the inline style throughout\n  // the entrance. A native opacity animation can expose the initial inline 0\n  // for a frame when it finishes, before Motion writes the final value.\n  const opacity = useMotionValue(0);\n  const ready = layout !== null;\n  useEffect(() => {\n    if (!ready) {\n      if (!isPresent) safeToRemove?.();\n      return;\n    }\n    const animation = animate(opacity, isPresent ? 1 : 0, {\n      ...(reduce ? { duration: 0.12 } : SPRING_PANEL),\n      onComplete: () => {\n        if (!isPresent) safeToRemove?.();\n      },\n    });\n    return () => animation.stop();\n  }, [opacity, ready, isPresent, reduce, safeToRemove]);\n\n  return (\n    <motion.div\n      data-morph-popover-portal=\"\"\n      inert={!isPresent}\n      // Wrapper carries the shadow as a drop-shadow filter, which hugs the\n      // clipped shape below (box-shadow would just get clipped away).\n      variants={wrap}\n      initial=\"hidden\"\n      animate={layout ? \"show\" : \"hidden\"}\n      exit=\"hidden\"\n      style={{\n        left,\n        top,\n        opacity,\n        pointerEvents: isPresent ? \"auto\" : \"none\",\n        visibility: layout ? \"visible\" : \"hidden\",\n        transformOrigin: originFor(side, align),\n      }}\n      className=\"fixed z-[9999] [filter:drop-shadow(0_10px_18px_rgba(0,0,0,0.14))]\"\n    >\n      <motion.div\n        ref={ctx.contentRef}\n        id={ctx.contentId}\n        role=\"dialog\"\n        aria-labelledby={ctx.triggerId}\n        variants={clip}\n        style={{ borderRadius: radius }}\n        className={cn(\n          \"overflow-hidden border border-border bg-background\",\n          className,\n        )}\n      >\n        {children}\n      </motion.div>\n    </motion.div>\n  );\n}\n"},{"path":"components/motion/select.tsx","type":"util","content":"\"use client\";\n\nimport { Check, ChevronDown } from \"lucide-react\";\nimport {\n  motion,\n  type Transition,\n  useReducedMotion,\n  type Variants,\n} from \"motion/react\";\nimport {\n  createContext,\n  type ReactNode,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nconst INSTANT_TRANSITION: Transition = { duration: 0 };\n\n// Spring with bounce powers the unfold/separation; per-property timings in the\n// content choreograph it (see SelectContent). Mirrors bouncy-accordion's feel.\nconst CHEVRON_TRANSITION: Transition = { type: \"spring\", duration: 0.4, bounce: 0.3 };\n\nconst LIST_VARIANTS: Variants = {\n  hidden: {},\n  show: { transition: { staggerChildren: 0.035, delayChildren: 0.05 } },\n};\nconst ITEM_VARIANTS: Variants = {\n  hidden: { opacity: 0, y: -6, filter: \"blur(3px)\" },\n  show: { opacity: 1, y: 0, filter: \"blur(0px)\" },\n};\n\ntype Placement = \"bottom\" | \"top\";\n\ninterface SelectContextValue {\n  value: string | undefined;\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  select: (value: string) => void;\n  register: (value: string, label: string) => void;\n  unregister: (value: string) => void;\n  labelFor: (value: string | undefined) => string | undefined;\n  reduce: boolean;\n  triggerId: string;\n  listId: string;\n  disabled: boolean;\n  placement: Placement;\n  setPlacement: (p: Placement) => void;\n}\n\nconst SelectContext = createContext<SelectContextValue | null>(null);\n\nfunction useSelectContext(component: string) {\n  const ctx = useContext(SelectContext);\n  if (!ctx) throw new Error(`${component} must be used within <Select>`);\n  return ctx;\n}\n\nexport interface SelectProps {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  /**\n   * Controlled open state of the panel. A layout that stacks selects can hold\n   * this to keep exactly one panel open — the panel is absolutely positioned\n   * inside its field, so two open at once paint over each other's options.\n   */\n  open?: boolean;\n  /** Uncontrolled initial open state. Default false. */\n  defaultOpen?: boolean;\n  /**\n   * Fires whenever the panel opens or closes. The panel is absolutely\n   * positioned inside the field, so a layout that stacks selects has to know\n   * which one is open to paint it above its neighbours.\n   */\n  onOpenChange?: (open: boolean) => void;\n  disabled?: boolean;\n  className?: string;\n  children: ReactNode;\n}\n\nexport function Select({\n  value,\n  defaultValue,\n  onValueChange,\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  disabled = false,\n  className,\n  children,\n}: SelectProps) {\n  const reduce = useReducedMotion() ?? false;\n  const baseId = useId();\n  const rootRef = useRef<HTMLDivElement>(null);\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const [internal, setInternal] = useState(defaultValue);\n  const [labels, setLabels] = useState<Map<string, string>>(new Map());\n  const [placement, setPlacement] = useState<Placement>(\"bottom\");\n\n  const controlled = value !== undefined;\n  const current = controlled ? value : internal;\n  const openControlled = openProp !== undefined;\n  const open = openControlled ? openProp : internalOpen;\n\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (!openControlled) setInternalOpen(next);\n      onOpenChange?.(next);\n    },\n    [onOpenChange, openControlled],\n  );\n\n  const select = useCallback(\n    (next: string) => {\n      if (!controlled) setInternal(next);\n      onValueChange?.(next);\n      setOpen(false);\n    },\n    [controlled, onValueChange, setOpen],\n  );\n\n  const register = useCallback((v: string, label: string) => {\n    setLabels((m) => (m.get(v) === label ? m : new Map(m).set(v, label)));\n  }, []);\n  const unregister = useCallback((v: string) => {\n    setLabels((m) => {\n      if (!m.has(v)) return m;\n      const next = new Map(m);\n      next.delete(v);\n      return next;\n    });\n  }, []);\n\n  // close on outside pointer / escape\n  useEffect(() => {\n    if (!open) return;\n    const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && setOpen(false);\n    const onPointer = (e: PointerEvent) => {\n      if (rootRef.current && !rootRef.current.contains(e.target as Node))\n        setOpen(false);\n    };\n    window.addEventListener(\"keydown\", onKey);\n    window.addEventListener(\"pointerdown\", onPointer);\n    return () => {\n      window.removeEventListener(\"keydown\", onKey);\n      window.removeEventListener(\"pointerdown\", onPointer);\n    };\n  }, [open, setOpen]);\n\n  const ctx = useMemo<SelectContextValue>(\n    () => ({\n      value: current,\n      open,\n      setOpen,\n      select,\n      register,\n      unregister,\n      labelFor: (v) => (v === undefined ? undefined : labels.get(v)),\n      reduce,\n      triggerId: `${baseId}-trigger`,\n      listId: `${baseId}-list`,\n      disabled,\n      placement,\n      setPlacement,\n    }),\n    [\n      current,\n      open,\n      setOpen,\n      select,\n      register,\n      unregister,\n      labels,\n      reduce,\n      baseId,\n      disabled,\n      placement,\n    ],\n  );\n\n  return (\n    <SelectContext.Provider value={ctx}>\n      <div ref={rootRef} className={cn(\"relative\", className)}>\n        {children}\n      </div>\n    </SelectContext.Provider>\n  );\n}\n\nexport interface SelectTriggerProps {\n  className?: string;\n  children: ReactNode;\n}\n\nexport function SelectTrigger({ className, children }: SelectTriggerProps) {\n  const ctx = useSelectContext(\"SelectTrigger\");\n  const isTop = ctx.placement === \"top\";\n  // edge facing the panel flattens then rounds; the far edge stays rounded.\n  // All four corners are specified so none gets stranded when placement flips.\n  const kf = ctx.open ? [0, 0, 12] : [12, 0, 12];\n  const kfT: Transition = ctx.reduce\n    ? { duration: 0 }\n    : ctx.open\n      ? { duration: 0.6, times: [0, 0.4, 1], ease: EASE_OUT }\n      : { duration: 0.42, times: [0, 0.5, 1], ease: EASE_OUT };\n  return (\n    <motion.button\n      type=\"button\"\n      id={ctx.triggerId}\n      disabled={ctx.disabled}\n      aria-haspopup=\"listbox\"\n      aria-expanded={ctx.open}\n      aria-controls={ctx.listId}\n      onClick={() => ctx.setOpen(!ctx.open)}\n      // Gooey: the edge facing the panel snaps flat (panel attached) then rounds\n      // back once the panel pulls away — the two pinch apart.\n      initial={false}\n      animate={{\n        borderTopLeftRadius: isTop ? kf : 12,\n        borderTopRightRadius: isTop ? kf : 12,\n        borderBottomLeftRadius: isTop ? 12 : kf,\n        borderBottomRightRadius: isTop ? 12 : kf,\n      }}\n      transition={{\n        borderTopLeftRadius: isTop ? kfT : INSTANT_TRANSITION,\n        borderTopRightRadius: isTop ? kfT : INSTANT_TRANSITION,\n        borderBottomLeftRadius: isTop ? INSTANT_TRANSITION : kfT,\n        borderBottomRightRadius: isTop ? INSTANT_TRANSITION : kfT,\n      }}\n      className={cn(\n        \"relative z-10 flex w-full items-center justify-between gap-2 rounded-xl border border-border bg-background px-3 py-2 text-sm text-foreground outline-none transition-colors\",\n        \"hover:border-(--color-border-strong) focus-visible:ring-2 focus-visible:ring-foreground/20\",\n        \"disabled:pointer-events-none disabled:opacity-50\",\n        className,\n      )}\n    >\n      {children}\n      <motion.span\n        aria-hidden\n        animate={{ rotate: ctx.open ? 180 : 0 }}\n        transition={ctx.reduce ? { duration: 0 } : CHEVRON_TRANSITION}\n        className=\"text-muted-foreground\"\n      >\n        <ChevronDown className=\"h-4 w-4\" />\n      </motion.span>\n    </motion.button>\n  );\n}\n\nexport interface SelectValueProps {\n  placeholder?: string;\n  className?: string;\n}\n\nexport function SelectValue({ placeholder, className }: SelectValueProps) {\n  const ctx = useSelectContext(\"SelectValue\");\n  const label = ctx.labelFor(ctx.value);\n  return (\n    <span\n      className={cn(label ? \"text-foreground\" : \"text-muted-foreground\", className)}\n    >\n      {label ?? placeholder ?? \"Select\"}\n    </span>\n  );\n}\n\nexport interface SelectContentProps {\n  className?: string;\n  children: ReactNode;\n}\n\nexport function SelectContent({ className, children }: SelectContentProps) {\n  const ctx = useSelectContext(\"SelectContent\");\n  const innerRef = useRef<HTMLDivElement>(null);\n  const [height, setHeight] = useState(0);\n  const open = ctx.open;\n  const { setPlacement } = ctx;\n\n  useLayoutEffect(() => {\n    const node = innerRef.current;\n    if (!node) return;\n    const measure = () => setHeight(node.offsetHeight);\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(node);\n    return () => observer.disconnect();\n  });\n\n  // On open, flip upward when there isn't room below and there's more above.\n  useLayoutEffect(() => {\n    if (!open) return;\n    const trigger = document.getElementById(ctx.triggerId);\n    const node = innerRef.current;\n    if (!trigger || !node) return;\n    const rect = trigger.getBoundingClientRect();\n    const h = node.offsetHeight;\n    const below = window.innerHeight - rect.bottom;\n    const above = rect.top;\n    setPlacement(below < h + 16 && above > below ? \"top\" : \"bottom\");\n  }, [open, ctx.triggerId, setPlacement]);\n\n  // Specify EVERY corner + both margins each render. The near edge (facing the\n  // trigger) animates flat->round and the gap opens on that side; the far edge\n  // stays rounded and its margin pinned to 0. Setting all of them avoids a\n  // stranded square corner when the placement flips between opens.\n  const isTop = ctx.placement === \"top\";\n  const nearGap = open ? 8 : 0;\n  const nearRadius = open ? 12 : 0;\n\n  const gapT: Transition = open\n    ? { type: \"spring\", duration: 0.6, bounce: 0.5, delay: 0.12 }\n    : { type: \"spring\", duration: 0.3, bounce: 0.1 };\n  const radiusT: Transition = open\n    ? { duration: 0.3, ease: EASE_OUT, delay: 0.14 }\n    : { duration: 0.16, ease: EASE_OUT };\n\n  // Items stay mounted (open just animates the panel) so each item's label\n  // registration persists — otherwise the trigger would fall back to the\n  // placeholder the moment the panel closes.\n  return (\n    <motion.div\n      id={ctx.listId}\n      role=\"listbox\"\n      aria-labelledby={ctx.triggerId}\n      aria-hidden={!open}\n      inert={!open}\n      initial={false}\n      animate={\n        ctx.reduce\n          ? { opacity: open ? 1 : 0, height: open ? height : 0 }\n          : {\n              opacity: open ? 1 : 0,\n              height: open ? height : 0,\n              // gap opens on the side facing the trigger\n              marginTop: isTop ? 0 : nearGap,\n              marginBottom: isTop ? nearGap : 0,\n              // near corners go flat->round; far corners stay rounded\n              borderTopLeftRadius: isTop ? 12 : nearRadius,\n              borderTopRightRadius: isTop ? 12 : nearRadius,\n              borderBottomLeftRadius: isTop ? nearRadius : 12,\n              borderBottomRightRadius: isTop ? nearRadius : 12,\n            }\n      }\n      transition={\n        ctx.reduce\n          ? { duration: 0.12 }\n          : {\n              opacity: open\n                ? { duration: 0.18 }\n                : { duration: 0.16, delay: 0.12 },\n              height: open\n                ? { type: \"spring\", duration: 0.42, bounce: 0.14 }\n                : { duration: 0.26, ease: EASE_OUT, delay: 0.14 },\n              marginTop: isTop ? INSTANT_TRANSITION : gapT,\n              marginBottom: isTop ? gapT : INSTANT_TRANSITION,\n              borderTopLeftRadius: isTop ? INSTANT_TRANSITION : radiusT,\n              borderTopRightRadius: isTop ? INSTANT_TRANSITION : radiusT,\n              borderBottomLeftRadius: isTop ? radiusT : INSTANT_TRANSITION,\n              borderBottomRightRadius: isTop ? radiusT : INSTANT_TRANSITION,\n            }\n      }\n      style={{\n        transformOrigin: isTop ? \"bottom\" : \"top\",\n        overflow: \"hidden\",\n        pointerEvents: open ? \"auto\" : \"none\",\n      }}\n      // flush against the trigger, then separates into its own rounded pill;\n      // sits above or below depending on available space\n      className={cn(\n        \"absolute left-0 right-0 z-20 rounded-xl border border-border bg-background shadow-lg\",\n        isTop ? \"bottom-full\" : \"top-full\",\n        className,\n      )}\n    >\n      <motion.div\n        ref={innerRef}\n        variants={ctx.reduce ? undefined : LIST_VARIANTS}\n        initial={false}\n        animate={open ? \"show\" : \"hidden\"}\n        className=\"p-1\"\n      >\n        {children}\n      </motion.div>\n    </motion.div>\n  );\n}\n\nexport interface SelectItemProps {\n  value: string;\n  disabled?: boolean;\n  className?: string;\n  children: ReactNode;\n}\n\nexport function SelectItem({\n  value,\n  disabled = false,\n  className,\n  children,\n}: SelectItemProps) {\n  const ctx = useSelectContext(\"SelectItem\");\n  const selected = ctx.value === value;\n  const label = typeof children === \"string\" ? children : value;\n\n  useLayoutEffect(() => {\n    ctx.register(value, label);\n    return () => ctx.unregister(value);\n  }, [ctx.register, ctx.unregister, value, label]);\n\n  return (\n    <motion.li variants={ctx.reduce ? undefined : ITEM_VARIANTS}>\n      <button\n        type=\"button\"\n        role=\"option\"\n        aria-selected={selected}\n        disabled={disabled}\n        onClick={() => ctx.select(value)}\n        className={cn(\n          \"flex w-full items-center justify-between gap-2 rounded-lg px-2.5 py-1.5 text-left text-sm outline-none transition-colors\",\n          selected\n            ? \"bg-muted text-foreground\"\n            : \"text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:bg-muted\",\n          \"disabled:pointer-events-none disabled:opacity-50\",\n          className,\n        )}\n      >\n        {children}\n        {selected ? <Check className=\"h-3.5 w-3.5 shrink-0\" /> : null}\n      </button>\n    </motion.li>\n  );\n}\n"},{"path":"components/agents/agent-disclosure.tsx","type":"util","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/citations.tsx","type":"util","content":"\"use client\";\n\nimport { BookOpenText, ChevronDown, ExternalLink, Globe2 } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  type ReactNode,\n  useCallback,\n  useId,\n  useState,\n} from \"react\";\nimport { AgentDisclosure } from \"@/components/agents/agent-disclosure\";\nimport { EASE_OUT, SPRING_LAYOUT, SPRING_SWAP } from \"@/lib/ease\";\nimport { useFavicon } from \"@/lib/hooks/use-favicon\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface CitationItem {\n  id: string;\n  title: ReactNode;\n  domain?: ReactNode;\n  url?: string;\n}\n\nexport interface CitationsProps {\n  citations: CitationItem[];\n  title?: ReactNode;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  idPrefix?: string;\n  className?: string;\n}\n\nexport interface CitationProps {\n  citationId: string;\n  index: number;\n  /** Must match the related Citations idPrefix. */\n  idPrefix: string;\n  className?: string;\n}\n\nexport interface CitationListProps {\n  citations: CitationItem[];\n  idPrefix?: string;\n  className?: string;\n}\n\nexport interface CitationStackProps {\n  citations: CitationItem[];\n  limit?: number;\n  className?: string;\n}\n\nfunction citationTargetId(prefix: string, citationId: string) {\n  return `${prefix}-${citationId.replace(/[^a-zA-Z0-9_-]/g, \"-\")}`;\n}\n\nexport function Citation({\n  citationId,\n  index,\n  idPrefix,\n  className,\n}: CitationProps) {\n  return (\n    <a\n      href={`#${citationTargetId(idPrefix, citationId)}`}\n      aria-label={`View citation ${index}`}\n      className={cn(\n        \"mx-0.5 inline-flex min-w-4 -translate-y-0.5 items-center justify-center rounded-md bg-muted/60 px-1 py-0.5 text-[10px] font-semibold leading-none text-muted-foreground no-underline outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\",\n        className,\n      )}\n    >\n      {index}\n    </a>\n  );\n}\n\nexport function CitationFavicon({\n  url,\n  className,\n}: {\n  url?: string;\n  className?: string;\n}) {\n  const favicon = useFavicon(url);\n\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={cn(\n        \"grid size-5 shrink-0 place-items-center text-muted-foreground\",\n        className,\n      )}\n    >\n      {favicon.src ? (\n        // biome-ignore lint/performance/noImgElement: Dynamic cross-site favicons keep this framework-agnostic registry component portable.\n        <img\n          ref={favicon.ref}\n          src={favicon.src}\n          alt=\"\"\n          width={16}\n          height={16}\n          referrerPolicy=\"no-referrer\"\n          className=\"size-4 rounded-sm object-contain\"\n        />\n      ) : (\n        <Globe2 className=\"size-3.5\" />\n      )}\n    </span>\n  );\n}\n\nexport function CitationStack({\n  citations,\n  limit = 3,\n  className,\n}: CitationStackProps) {\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={cn(\"flex -space-x-1.5\", className)}\n    >\n      {citations.slice(0, limit).map((citation) => (\n        <CitationFavicon\n          key={citation.id}\n          url={citation.url}\n          className=\"size-6 rounded-full bg-background ring-2 ring-background\"\n        />\n      ))}\n    </span>\n  );\n}\n\nfunction CitationRow({\n  citation,\n  index,\n  idPrefix,\n}: {\n  citation: CitationItem;\n  index: number;\n  idPrefix: string;\n}) {\n  const content = (\n    <>\n      <CitationFavicon url={citation.url} />\n      <span className=\"flex min-w-0 flex-1 flex-wrap items-baseline gap-x-2 gap-y-0.5\">\n        <span className=\"truncate text-sm font-medium text-foreground/80 transition-colors group-hover/citation:text-foreground\">\n          {citation.title}\n        </span>\n        {citation.domain ? (\n          <span className=\"min-w-0 truncate text-xs text-muted-foreground/60\">\n            {citation.domain}\n          </span>\n        ) : null}\n      </span>\n      <span className=\"flex shrink-0 items-center gap-1.5\">\n        <span className=\"grid size-5 place-items-center rounded-md bg-foreground/[0.05] text-[10px] font-semibold tabular-nums text-muted-foreground\">\n          {index}\n        </span>\n        {citation.url ? (\n          <ExternalLink className=\"size-3.5 text-muted-foreground/40 transition-colors group-hover/citation:text-muted-foreground\" />\n        ) : null}\n      </span>\n    </>\n  );\n  const className =\n    \"group/citation flex items-center gap-2 rounded-md px-1.5 py-1 outline-none focus-visible:ring-2 focus-visible:ring-ring\";\n  const id = citationTargetId(idPrefix, citation.id);\n\n  return citation.url ? (\n    <a\n      id={id}\n      href={citation.url}\n      target=\"_blank\"\n      rel=\"noreferrer noopener\"\n      className={className}\n    >\n      {content}\n    </a>\n  ) : (\n    <div id={id} className={className}>\n      {content}\n    </div>\n  );\n}\n\nexport function CitationList({\n  citations,\n  idPrefix,\n  className,\n}: CitationListProps) {\n  const reduce = useReducedMotion() ?? false;\n  const baseId = useId();\n  const resolvedPrefix =\n    idPrefix ?? `citation-list-${baseId.replace(/:/g, \"\")}`;\n\n  return (\n    <div className={cn(\"grid gap-0.5\", className)}>\n      <AnimatePresence mode=\"popLayout\">\n        {citations.map((citation, index) => (\n          <motion.div\n            layout=\"position\"\n            key={citation.id}\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            <CitationRow\n              citation={citation}\n              index={index + 1}\n              idPrefix={resolvedPrefix}\n            />\n          </motion.div>\n        ))}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nexport function Citations({\n  citations,\n  title = \"Sources\",\n  open,\n  defaultOpen = false,\n  onOpenChange,\n  idPrefix,\n  className,\n}: CitationsProps) {\n  const reduce = useReducedMotion() ?? false;\n  const baseId = useId();\n  const contentId = `${baseId}-content`;\n  const resolvedPrefix =\n    idPrefix ?? `citation-${baseId.replace(/:/g, \"\")}`;\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const currentOpen = open ?? internalOpen;\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (open === undefined) setInternalOpen(next);\n      onOpenChange?.(next);\n    },\n    [onOpenChange, open],\n  );\n\n  return (\n    <div className={cn(\"w-full text-sm\", className)}>\n      <button\n        type=\"button\"\n        aria-expanded={currentOpen}\n        aria-controls={contentId}\n        onClick={() => setOpen(!currentOpen)}\n        className=\"group -ml-1 flex min-h-8 items-center gap-2 rounded-lg px-1 text-left text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n      >\n        <BookOpenText className=\"size-4\" />\n        <span className=\"font-medium\">{title}</span>\n        <span className=\"rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-semibold tabular-nums\">\n          {citations.length}\n        </span>\n        <motion.span\n          aria-hidden=\"true\"\n          animate={{ rotate: currentOpen ? 180 : 0 }}\n          transition={reduce ? { duration: 0 } : SPRING_SWAP}\n          className=\"text-muted-foreground/60\"\n        >\n          <ChevronDown className=\"size-3.5\" />\n        </motion.span>\n      </button>\n\n      <AgentDisclosure\n        id={contentId}\n        open={currentOpen}\n      >\n        <CitationList\n          citations={citations}\n          idPrefix={resolvedPrefix}\n          className=\"mt-1\"\n        />\n      </AgentDisclosure>\n    </div>\n  );\n}\n"},{"path":"components/motion/button/base.tsx","type":"util","content":"\"use client\";\n\nimport {\n  AnimatePresence,\n  type HTMLMotionProps,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  forwardRef,\n  type PointerEvent,\n  type ReactNode,\n  useCallback,\n  useRef,\n  useState,\n} from \"react\";\nimport { EASE_OUT, SPRING_PRESS } from \"@/lib/ease\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"ghost\" | \"outline\";\nexport type ButtonSize = \"sm\" | \"md\" | \"lg\" | \"icon\";\n\nexport interface ButtonProps extends Omit<\n  HTMLMotionProps<\"button\">,\n  \"children\"\n> {\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n  pressScale?: number;\n  /** Spawn a Material-style ripple from the press point. Off by default. */\n  ripple?: boolean;\n  children?: ReactNode;\n}\n\nexport interface ButtonLinkProps extends Omit<\n  HTMLMotionProps<\"a\">,\n  \"children\"\n> {\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n  pressScale?: number;\n  children?: ReactNode;\n}\n\ntype Ripple = { id: number; x: number; y: number; size: number };\n\nconst VARIANT_CLASS: Record<ButtonVariant, string> = {\n  primary: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n  secondary: \"border border-border bg-card text-foreground hover:border-border\",\n  ghost: \"text-muted-foreground hover:text-foreground hover:bg-muted/60\",\n  outline:\n    \"border border-border bg-transparent text-foreground hover:bg-muted/60\",\n};\n\nconst SIZE_CLASS: Record<ButtonSize, string> = {\n  sm: \"h-8 px-3 text-xs gap-1.5 rounded-full\",\n  md: \"h-10 px-5 text-sm gap-2 rounded-full\",\n  lg: \"h-12 px-6 text-base gap-2 rounded-full\",\n  icon: \"h-8 w-8 rounded-lg\",\n};\n\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(\n  function Button(\n    {\n      variant = \"primary\",\n      size = \"md\",\n      pressScale = 0.93,\n      ripple = false,\n      className,\n      children,\n      onPointerDown,\n      ...rest\n    },\n    ref,\n  ) {\n    const reduce = useReducedMotion();\n    const canHover = useHoverCapable();\n    const [ripples, setRipples] = useState<Ripple[]>([]);\n    const nextId = useRef(0);\n\n    const handlePointerDown = useCallback(\n      (event: PointerEvent<HTMLButtonElement>) => {\n        if (ripple && !reduce) {\n          const rect = event.currentTarget.getBoundingClientRect();\n          const size = Math.max(rect.width, rect.height) * 2;\n          const id = nextId.current++;\n          setRipples((prev) => [\n            ...prev,\n            {\n              id,\n              x: event.clientX - rect.left,\n              y: event.clientY - rect.top,\n              size,\n            },\n          ]);\n        }\n        onPointerDown?.(event);\n      },\n      [ripple, reduce, onPointerDown],\n    );\n\n    return (\n      <motion.button\n        ref={ref}\n        type=\"button\"\n        whileTap={reduce ? undefined : { scale: pressScale }}\n        whileHover={reduce || !canHover ? undefined : { scale: 1.02 }}\n        transition={SPRING_PRESS}\n        onPointerDown={handlePointerDown}\n        className={cn(\n          \"inline-flex items-center justify-center font-medium select-none\",\n          \"transition-colors\",\n          \"disabled:pointer-events-none disabled:opacity-50\",\n          ripple && \"relative overflow-hidden\",\n          VARIANT_CLASS[variant],\n          SIZE_CLASS[size],\n          className,\n        )}\n        {...rest}\n      >\n        {ripple && !reduce ? (\n          <span className=\"pointer-events-none absolute inset-0 overflow-hidden rounded-[inherit]\">\n            <AnimatePresence>\n              {ripples.map((r) => (\n                <motion.span\n                  key={r.id}\n                  className=\"absolute rounded-full bg-current\"\n                  style={{\n                    left: r.x,\n                    top: r.y,\n                    width: r.size,\n                    height: r.size,\n                    x: \"-50%\",\n                    y: \"-50%\",\n                  }}\n                  initial={{ scale: 0.05, opacity: 0.3 }}\n                  animate={{ scale: 1, opacity: 0 }}\n                  exit={{ opacity: 0 }}\n                  transition={{ duration: 1.6, ease: EASE_OUT }}\n                  onAnimationComplete={() =>\n                    setRipples((prev) => prev.filter((x) => x.id !== r.id))\n                  }\n                />\n              ))}\n            </AnimatePresence>\n          </span>\n        ) : null}\n        {children}\n      </motion.button>\n    );\n  },\n);\n\nexport const ButtonLink = forwardRef<HTMLAnchorElement, ButtonLinkProps>(\n  function ButtonLink(\n    {\n      variant = \"primary\",\n      size = \"md\",\n      pressScale = 0.93,\n      className,\n      children,\n      ...rest\n    },\n    ref,\n  ) {\n    const reduce = useReducedMotion();\n    const canHover = useHoverCapable();\n\n    return (\n      <motion.a\n        ref={ref}\n        whileTap={reduce ? undefined : { scale: pressScale }}\n        whileHover={reduce || !canHover ? undefined : { scale: 1.02 }}\n        transition={SPRING_PRESS}\n        className={cn(\n          \"inline-flex items-center justify-center font-medium select-none\",\n          \"transition-colors\",\n          VARIANT_CLASS[variant],\n          SIZE_CLASS[size],\n          className,\n        )}\n        {...rest}\n      >\n        {children}\n      </motion.a>\n    );\n  },\n);\n"},{"path":"components/motion/button/magnetic.tsx","type":"util","content":"\"use client\";\n\nimport { forwardRef } from \"react\";\nimport { Magnetic } from \"../magnetic\";\nimport { Button, type ButtonProps } from \"./base\";\n\nexport interface MagneticButtonProps extends ButtonProps {\n  /** Magnetic pull strength. Default 0.25. */\n  strength?: number;\n  /** Class applied to the magnetic wrapper. */\n  magneticClassName?: string;\n}\n\nexport const MagneticButton = forwardRef<HTMLButtonElement, MagneticButtonProps>(function MagneticButton(\n  { strength = 0.25, magneticClassName, children, ...rest },\n  ref,\n) {\n  return (\n    <Magnetic strength={strength} className={magneticClassName}>\n      <Button ref={ref} {...rest}>\n        {children}\n      </Button>\n    </Magnetic>\n  );\n});\n"},{"path":"components/motion/button/metallic.tsx","type":"util","content":"\"use client\";\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { forwardRef, useState } from \"react\";\nimport { EASE_IN_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\nimport { Button, type ButtonProps } from \"./base\";\n\nexport interface MetallicButtonProps extends Omit<\n  ButtonProps,\n  \"ripple\" | \"variant\"\n> {\n  /** Stops the traveling reflection while preserving the chrome rim. */\n  paused?: boolean;\n}\n\n// The rim and highlight drift separately so the material stays quiet and reflective.\nconst SILVER_DRIFT = {\n  duration: 8,\n  ease: EASE_IN_OUT,\n  repeat: Infinity,\n};\n\nconst CHROME_SHIMMER = {\n  duration: 2.4,\n  ease: EASE_IN_OUT,\n};\n\nexport const MetallicButton = forwardRef<\n  HTMLButtonElement,\n  MetallicButtonProps\n>(function MetallicButton(\n  {\n    size = \"md\",\n    paused = false,\n    className,\n    children,\n    onHoverStart,\n    onHoverEnd,\n    ...rest\n  },\n  ref,\n) {\n  const reduce = useReducedMotion();\n  const still = paused || Boolean(reduce);\n  const [hovered, setHovered] = useState(false);\n\n  return (\n    <Button\n      ref={ref}\n      variant=\"ghost\"\n      size={size}\n      onHoverStart={(event, info) => {\n        setHovered(true);\n        onHoverStart?.(event, info);\n      }}\n      onHoverEnd={(event, info) => {\n        setHovered(false);\n        onHoverEnd?.(event, info);\n      }}\n      className={cn(\n        \"group relative isolate overflow-hidden border-0 bg-transparent text-foreground\",\n        \"hover:bg-transparent hover:text-foreground\",\n        \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n        \"shadow-[0_8px_22px_rgba(0,0,0,0.16)]\",\n        size === \"icon\" && \"rounded-full\",\n        className,\n      )}\n      {...rest}\n    >\n      <motion.span\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-y-0 left-[-18%] z-0 w-[136%] rounded-[inherit] bg-[linear-gradient(105deg,#111_0%,#737373_14%,#fafafa_26%,#525252_38%,#0a0a0a_50%,#a3a3a3_64%,#fff_75%,#404040_87%,#111_100%)]\"\n        animate={still ? undefined : { x: [\"0%\", \"13%\", \"0%\"] }}\n        transition={still ? undefined : SILVER_DRIFT}\n      />\n\n      <motion.span\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-y-0 left-[-58%] z-[1] w-[52%] -skew-x-12 bg-[linear-gradient(90deg,transparent,rgba(255,255,255,0.5)_48%,transparent)] opacity-50 blur-[3px] mix-blend-screen\"\n        animate={still ? undefined : { x: hovered ? \"310%\" : \"0%\" }}\n        transition={still ? undefined : CHROME_SHIMMER}\n      />\n\n      <span\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-[2px] z-[2] rounded-[inherit] bg-background transition-colors group-hover:bg-muted/40\"\n      />\n\n      <span\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-[2px] z-[3] rounded-[inherit] shadow-[inset_0_1px_0_rgba(255,255,255,0.28),inset_0_-1px_0_rgba(0,0,0,0.16)]\"\n      />\n\n      <span className=\"relative z-10 inline-flex items-center justify-center gap-2\">\n        {children}\n      </span>\n    </Button>\n  );\n});\n"},{"path":"components/motion/button/stateful.tsx","type":"util","content":"\"use client\";\n\nimport { Check, Loader2, X } from \"lucide-react\";\nimport {\n  AnimatePresence,\n  motion,\n  useReducedMotion,\n  type Variants,\n} from \"motion/react\";\nimport {\n  forwardRef,\n  type ReactNode,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { EASE_OUT, SPRING_SWAP } from \"@/lib/ease\";\nimport { Button, type ButtonProps } from \"./base\";\n\nexport type ButtonState = \"idle\" | \"loading\" | \"success\" | \"error\";\n\nexport interface StatefulButtonProps extends Omit<ButtonProps, \"children\"> {\n  state?: ButtonState;\n  children: ReactNode;\n  loadingText?: ReactNode;\n  successText?: ReactNode;\n  errorText?: ReactNode;\n  icon?: ReactNode;\n}\n\nconst CASCADE_STAGGER = 0.025;\nconst ROLL_BLUR = \"blur(6px)\";\n\nconst CASCADE_LETTER_VARIANTS: Variants = {\n  initial: { opacity: 0, y: \"105%\", filter: ROLL_BLUR },\n  animate: (delay: number = 0) => ({\n    opacity: 1,\n    y: \"0%\",\n    filter: \"blur(0px)\",\n    transition: { ...SPRING_SWAP, delay },\n  }),\n  exit: (delay: number = 0) => ({\n    opacity: 0,\n    y: \"-105%\",\n    filter: ROLL_BLUR,\n    transition: { duration: 0.16, ease: EASE_OUT, delay: delay * 0.5 },\n  }),\n};\n\nconst ICON_VARIANTS: Variants = {\n  // Width collapses too, so the icon adds/removes its own space smoothly\n  // instead of popping the row width in a single frame.\n  initial: { opacity: 0, width: 0, scale: 0.7, filter: ROLL_BLUR },\n  animate: {\n    opacity: 1,\n    width: \"1.5rem\",\n    scale: 1,\n    filter: \"blur(0px)\",\n    transition: SPRING_SWAP,\n  },\n  exit: {\n    opacity: 0,\n    width: 0,\n    scale: 0.7,\n    filter: ROLL_BLUR,\n    transition: { duration: 0.16, ease: EASE_OUT },\n  },\n};\n\nfunction IconSlot({ keyId, children }: { keyId: string; children: ReactNode }) {\n  const reduce = useReducedMotion();\n  return (\n    <motion.span\n      key={keyId}\n      variants={ICON_VARIANTS}\n      initial={reduce ? { opacity: 0 } : \"initial\"}\n      animate={reduce ? { opacity: 1 } : \"animate\"}\n      exit={reduce ? { opacity: 0 } : \"exit\"}\n      transition={reduce ? { duration: 0.15 } : undefined}\n      className=\"inline-grid shrink-0 place-items-center overflow-hidden\"\n    >\n      {children}\n    </motion.span>\n  );\n}\n\nfunction TextSlot({\n  value,\n  children,\n}: {\n  value: string;\n  children: ReactNode;\n}) {\n  const reduce = useReducedMotion();\n  const measureRef = useRef<HTMLSpanElement>(null);\n  const [width, setWidth] = useState<number>();\n  const label = typeof children === \"string\" ? children : null;\n  const cascade = label !== null && !reduce;\n\n  // Measure strings with the same per-letter layout as the cascade. Measuring\n  // the whole string preserves kerning, which can make it narrower than the\n  // inline-block letters and clip the final glyph during the width animation.\n  useLayoutEffect(() => {\n    const nextWidth = measureRef.current?.offsetWidth;\n    if (!nextWidth) return;\n    setWidth((current) => (current === nextWidth ? current : nextWidth));\n  });\n\n  return (\n    <motion.span\n      initial={false}\n      animate={{ width }}\n      transition={reduce ? { duration: 0 } : SPRING_SWAP}\n      className=\"relative inline-block overflow-hidden whitespace-nowrap align-bottom\"\n    >\n      <span\n        ref={measureRef}\n        aria-hidden\n        className=\"invisible inline-block whitespace-nowrap\"\n      >\n        {cascade\n          ? label.split(\"\").map((char, index) => (\n              <span\n                // biome-ignore lint/suspicious/noArrayIndexKey: position is the slot identity.\n                key={index}\n                className=\"inline-block whitespace-pre\"\n              >\n                {char}\n              </span>\n            ))\n          : children}\n      </span>\n\n      {cascade ? (\n        <>\n          <span className=\"sr-only\">{label}</span>\n          <AnimatePresence initial={false}>\n            <motion.span\n              key={`cascade-${value}`}\n              aria-hidden\n              initial=\"initial\"\n              animate=\"animate\"\n              exit=\"exit\"\n              className=\"absolute left-0 top-0 inline-block whitespace-pre\"\n            >\n              {label.split(\"\").map((char, index) => (\n                <motion.span\n                  // biome-ignore lint/suspicious/noArrayIndexKey: position is the slot identity.\n                  key={index}\n                  custom={index * CASCADE_STAGGER}\n                  variants={CASCADE_LETTER_VARIANTS}\n                  className=\"inline-block whitespace-pre will-change-[opacity,filter,transform]\"\n                >\n                  {char}\n                </motion.span>\n              ))}\n            </motion.span>\n          </AnimatePresence>\n        </>\n      ) : (\n        <AnimatePresence initial={false}>\n          <motion.span\n            key={`text-${value}`}\n            initial={reduce ? { opacity: 0 } : { opacity: 0, y: 14, filter: ROLL_BLUR }}\n            animate={reduce ? { opacity: 1 } : { opacity: 1, y: 0, filter: \"blur(0px)\" }}\n            exit={reduce ? { opacity: 0 } : { opacity: 0, y: -14, filter: ROLL_BLUR }}\n            transition={reduce ? { duration: 0.15 } : SPRING_SWAP}\n            className=\"absolute left-0 top-0 inline-block will-change-[opacity,filter,transform]\"\n          >\n            {children}\n          </motion.span>\n        </AnimatePresence>\n      )}\n    </motion.span>\n  );\n}\n\nexport const StatefulButton = forwardRef<HTMLButtonElement, StatefulButtonProps>(function StatefulButton(\n  {\n    state = \"idle\",\n    children,\n    loadingText = \"Loading\",\n    successText = \"Done\",\n    errorText = \"Try again\",\n    icon,\n    disabled,\n    ...rest\n  },\n  ref,\n) {\n  const isBusy = state === \"loading\";\n  const stateText =\n    state === \"loading\"\n      ? loadingText\n      : state === \"success\"\n        ? successText\n        : state === \"error\"\n        ? errorText\n        : children;\n  const textKey =\n    typeof stateText === \"string\" ? `${state}-${stateText}` : state;\n\n  return (\n    <Button ref={ref} disabled={disabled || isBusy} aria-busy={isBusy} whileHover={undefined} {...rest}>\n      <span\n        aria-live=\"polite\"\n        className=\"relative inline-flex items-center justify-center overflow-hidden\"\n      >\n        <AnimatePresence initial={false}>\n          {state === \"loading\" ? (\n            <IconSlot keyId=\"loading-icon\">\n              <Loader2 className=\"h-4 w-4 animate-spin\" />\n            </IconSlot>\n          ) : null}\n          {state === \"success\" ? (\n            <IconSlot keyId=\"success-icon\">\n              <Check className=\"h-4 w-4\" />\n            </IconSlot>\n          ) : null}\n          {state === \"error\" ? (\n            <IconSlot keyId=\"error-icon\">\n              <X className=\"h-4 w-4\" />\n            </IconSlot>\n          ) : null}\n        </AnimatePresence>\n\n        <TextSlot value={textKey}>{stateText}</TextSlot>\n\n        <AnimatePresence initial={false}>\n          {state === \"idle\" && icon ? (\n            <IconSlot keyId=\"idle-icon\">{icon}</IconSlot>\n          ) : null}\n        </AnimatePresence>\n      </span>\n    </Button>\n  );\n});\n"},{"path":"components/motion/popover-position.ts","type":"util","content":"\"use client\";\n\nimport {\n  type MutableRefObject,\n  useCallback,\n  useLayoutEffect,\n  useState,\n} from \"react\";\n\nexport type PortalLayout = {\n  trigger: {\n    left: number;\n    top: number;\n    width: number;\n    height: number;\n  };\n  content: {\n    width: number;\n    height: number;\n  };\n};\n\nfunction sameLayout(a: PortalLayout | null, b: PortalLayout) {\n  return (\n    a?.trigger.left === b.trigger.left &&\n    a.trigger.top === b.trigger.top &&\n    a.trigger.width === b.trigger.width &&\n    a.trigger.height === b.trigger.height &&\n    a.content.width === b.content.width &&\n    a.content.height === b.content.height\n  );\n}\n\n/** Measures a trigger and portalled panel in viewport coordinates. */\nexport function usePopoverPortalPosition<\n  TriggerElement extends HTMLElement,\n  ContentElement extends HTMLElement,\n>(\n  triggerRef: MutableRefObject<TriggerElement | null>,\n  contentRef: MutableRefObject<ContentElement | null>,\n  active: boolean,\n) {\n  const [layout, setLayout] = useState<PortalLayout | null>(null);\n\n  const update = useCallback(() => {\n    const trigger = triggerRef.current;\n    const content = contentRef.current;\n    if (!trigger || !content) return;\n\n    const rect = trigger.getBoundingClientRect();\n    const next: PortalLayout = {\n      trigger: {\n        left: rect.left,\n        top: rect.top,\n        width: rect.width,\n        height: rect.height,\n      },\n      content: {\n        width: content.offsetWidth,\n        height: content.offsetHeight,\n      },\n    };\n    setLayout((current) => (sameLayout(current, next) ? current : next));\n  }, [contentRef, triggerRef]);\n\n  useLayoutEffect(() => {\n    update();\n    if (!active) return;\n\n    const trigger = triggerRef.current;\n    const content = contentRef.current;\n    const observer = new ResizeObserver(update);\n    if (trigger) observer.observe(trigger);\n    if (content) observer.observe(content);\n\n    window.addEventListener(\"scroll\", update, true);\n    window.addEventListener(\"resize\", update);\n    return () => {\n      observer.disconnect();\n      window.removeEventListener(\"scroll\", update, true);\n      window.removeEventListener(\"resize\", update);\n    };\n  }, [active, contentRef, triggerRef, update]);\n\n  return layout;\n}\n"},{"path":"lib/hooks/use-favicon.ts","type":"util","content":"\"use client\";\n\nimport { useCallback, useState } from \"react\";\nimport { getFaviconUrl } from \"@/lib/favicon\";\n\n/**\n * Resolves a site favicon and drops it once it is known to be unusable, so\n * callers can draw their own glyph instead of a broken image. Plenty of sites\n * answer `/favicon.ico` with a 403 or 404, and `onError` is not enough to catch\n * it: an image the browser starts loading from server-rendered HTML usually\n * fails before React attaches a handler, and that event is never replayed.\n * `decode()` settles on the image's final state instead of relying on an event\n * firing at the right moment.\n */\nexport function useFavicon(url?: string) {\n  const resolved = url ? getFaviconUrl(url) : null;\n  const [failedSrc, setFailedSrc] = useState<string | null>(null);\n  const src = resolved && resolved !== failedSrc ? resolved : null;\n\n  const ref = useCallback(\n    (img: HTMLImageElement | null) => {\n      if (!img || !src) return;\n\n      let released = false;\n      img.decode().catch(() => {\n        if (!released) setFailedSrc(src);\n      });\n\n      // The node is going away or the source changed; a late rejection then\n      // describes an image we are no longer showing.\n      return () => {\n        released = true;\n      };\n    },\n    [src],\n  );\n\n  return { src, ref };\n}\n"},{"path":"lib/hooks/use-hover-capable.ts","type":"util","content":"\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\n/**\n * Returns true only on devices that have a true hover (mouse / trackpad).\n * Touch devices fire phantom `:hover` on tap that sticks until tap-elsewhere\n * — gate hover-only effects (scale lifts, magnetic pulls) behind this.\n */\nexport function useHoverCapable() {\n  const [canHover, setCanHover] = useState(false);\n\n  useEffect(() => {\n    if (typeof window === \"undefined\" || !window.matchMedia) return;\n    const mq = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    const update = () => setCanHover(mq.matches);\n    update();\n    mq.addEventListener?.(\"change\", update);\n    return () => mq.removeEventListener?.(\"change\", update);\n  }, []);\n\n  return canHover;\n}\n"},{"path":"components/motion/magnetic.tsx","type":"util","content":"\"use client\";\n\nimport { motion, useMotionValue, useReducedMotion, useSpring } from \"motion/react\";\nimport { useRef, type ReactNode } from \"react\";\nimport { SPRING_MOUSE } from \"@/lib/ease\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface MagneticProps {\n  children: ReactNode;\n  strength?: number;\n  className?: string;\n}\n\nexport function Magnetic({ children, strength = 0.35, className }: MagneticProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const reduce = useReducedMotion();\n  const canHover = useHoverCapable();\n  // Decorative cursor-follow: skip on touch (phantom hover) and reduced motion.\n  const enabled = !reduce && canHover;\n  const x = useMotionValue(0);\n  const y = useMotionValue(0);\n  const sx = useSpring(x, SPRING_MOUSE);\n  const sy = useSpring(y, SPRING_MOUSE);\n\n  const onMove = (e: React.MouseEvent<HTMLDivElement>) => {\n    const el = ref.current;\n    if (!el || !enabled) return;\n    const rect = el.getBoundingClientRect();\n    x.set((e.clientX - rect.left - rect.width / 2) * strength);\n    y.set((e.clientY - rect.top - rect.height / 2) * strength);\n  };\n\n  const onLeave = () => {\n    x.set(0);\n    y.set(0);\n  };\n\n  return (\n    <motion.div\n      ref={ref}\n      onMouseMove={onMove}\n      onMouseLeave={onLeave}\n      style={{ x: sx, y: sy }}\n      className={cn(\"inline-block\", className)}\n    >\n      {children}\n    </motion.div>\n  );\n}\n"},{"path":"lib/favicon.ts","type":"util","content":"/** Resolve a website URL to its conventional root favicon location. */\nexport function getFaviconUrl(value: string) {\n  try {\n    return new URL(\"/favicon.ico\", value).toString();\n  } catch {\n    return null;\n  }\n}\n"}]}