{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "components-chat",
  "type": "registry:component",
  "title": "Chat",
  "description": "A conversation with a following transcript",
  "author": "sudo-kw <https://github.com/sudo-kw>",
  "dependencies": [
    "@gpuix/native@0.11.0",
    "@gpuix/react@0.11.0",
    "effect@4.1.2",
    "react@^19.2.4"
  ],
  "registryDependencies": [
    "https://gpuixui.sudokw.com/gpuix-icons.json",
    "https://gpuixui.sudokw.com/gpuix-primitives.json",
    "https://gpuixui.sudokw.com/gpuix-tokens.json",
    "https://gpuixui.sudokw.com/gpuix-tw.json"
  ],
  "files": [
    {
      "path": "packages/components/src/chat.tsx",
      "target": "@ui/gpuixui/components/chat.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { KeyEvent, Props } from \"@gpuix/react\";\nimport {\n  mergeStyles,\n  resolveStyle,\n  type StateStyle,\n} from \"@gpuix/react/floating\";\nimport {\n  CHAT_COMPOSER,\n  CHAT_COMPOSER_ATTACHMENTS,\n  CHAT_COMPOSER_FIELD,\n  CHAT_COMPOSER_HINT,\n  CHAT_COMPOSER_ROW,\n  CHAT_EMPTY,\n  CHAT_EMPTY_DESCRIPTION,\n  CHAT_EMPTY_TITLE,\n  CHAT_HEADER,\n  CHAT_HEADER_ACTIONS,\n  CHAT_HEADER_COPY,\n  CHAT_HEADER_STATUS,\n  CHAT_HEADER_TITLE,\n  CHAT_MESSAGES,\n  CHAT_ROOT,\n  cn,\n} from \"../tw/index.js\";\nimport {\n  accessibleName,\n  spokenSentence,\n  visibleCopy,\n} from \"../primitives/core/index.js\";\nimport { Icon } from \"../icons/index.js\";\nimport * as Option from \"effect/Option\";\nimport {\n  Children,\n  createContext,\n  isValidElement,\n  useCallback,\n  useContext,\n  useMemo,\n  type ReactNode,\n} from \"react\";\nimport * as Machine from \"./chat/logic.js\";\n\nexport * from \"./chat/logic.js\";\nimport { Button, actionOnColor } from \"./internal/button.js\";\nimport { useControllableState } from \"./internal/controllable.js\";\nimport {\n  fieldHelp,\n  fieldHint,\n  hintHost,\n  renderFieldHint,\n} from \"../primitives/components/hint.js\";\nimport { isDismiss } from \"./internal/key.js\";\nimport {\n  assertParts,\n  type PartChildren,\n  type PartComponent,\n  type PartsContract,\n} from \"./internal/parts.js\";\nimport { recipeStyle } from \"./internal/recipe.js\";\nimport {\n  keepContent,\n  keyboardRing,\n  nativeTextTheme,\n  selectionWash,\n  statusTone,\n  useTheme,\n} from \"./internal/theme.js\";\nimport {\n  MessageScroller,\n  MessageScrollerContent,\n  MessageScrollerItem,\n  MessageScrollerJumpToLatest,\n  MessageScrollerViewport,\n} from \"./message-scroller.js\";\nimport { textOf } from \"./internal/text.js\";\n\nconst DEFAULT_CONVERSATION_LABEL = \"Conversation\";\nconst DEFAULT_ERROR = \"The reply failed.\";\nconst DEFAULT_EMPTY_TITLE = \"Start the conversation\";\nconst DEFAULT_EMPTY_DESCRIPTION = \"Messages you send appear here.\";\nconst DEFAULT_COMPOSER_LABEL = \"Message\";\nconst DEFAULT_SUBMIT_LABEL = \"Send\";\nconst DEFAULT_STOP_LABEL = \"Stop\";\n\ninterface ChatContextValue {\n  readonly status: Machine.ChatStatus;\n\n  readonly label: string;\n\n  readonly error: string;\n\n  readonly canStop: boolean;\n\n  readonly canRetry: boolean;\n\n  readonly submit: (message: string) => void;\n\n  readonly stop: () => void;\n\n  readonly retry: () => void;\n}\n\nconst ChatContext = createContext<ChatContextValue | undefined>(undefined);\n\nconst useChat = (): ChatContextValue => {\n  const value = useContext(ChatContext);\n  if (value === undefined) {\n    throw new Error(\"Chat parts must be used inside ChatRoot\");\n  }\n  return value;\n};\n\nexport interface ChatRootProps extends Omit<Props, \"children\" | \"onSubmit\"> {\n  readonly status?: Machine.ChatStatus;\n\n  readonly defaultStatus?: Machine.ChatStatus;\n  readonly onStatusChange?: (status: Machine.ChatStatus) => void;\n\n  readonly onSubmit: (message: string) => void;\n\n  readonly onStop?: () => void;\n\n  readonly onRetry?: () => void;\n\n  readonly error?: string;\n\n  readonly label?: string;\n\n  readonly className?: string;\n  readonly children?: PartChildren;\n}\n\nexport const ChatRoot = ({\n  status: statusProp,\n  defaultStatus = \"idle\",\n  onStatusChange,\n  onSubmit,\n  onStop,\n  onRetry,\n  error,\n  label,\n  className,\n  children,\n  style,\n  ...rest\n}: ChatRootProps) => {\n  const theme = useTheme();\n\n  const name = accessibleName(textOf(label)) ?? DEFAULT_CONVERSATION_LABEL;\n  const failure = spokenSentence(textOf(error), DEFAULT_ERROR);\n  const [status, setStatus] = useControllableState({\n    value: statusProp,\n    defaultValue: defaultStatus,\n    onChange: onStatusChange,\n  });\n  assertParts(chatRoot, children);\n\n  const submit = useCallback(\n    (message: string) => {\n      const move = Machine.chatMove(status, \"submit\");\n      if (Option.isNone(move.emit)) return;\n      setStatus(move.status);\n      onSubmit(message);\n    },\n    [onSubmit, setStatus, status],\n  );\n\n  const stop = useCallback(() => {\n    const move = Machine.chatMove(status, \"stop\");\n    if (Option.isNone(move.emit)) return;\n    setStatus(move.status);\n    onStop?.();\n  }, [onStop, setStatus, status]);\n\n  const retry = useCallback(() => {\n    const move = Machine.chatMove(status, \"retry\");\n    if (Option.isNone(move.emit)) return;\n    setStatus(move.status);\n    onRetry?.();\n  }, [onRetry, setStatus, status]);\n\n  const context = useMemo<ChatContextValue>(\n    () => ({\n      status,\n      label: name,\n      error: failure,\n      canStop: onStop !== undefined,\n      canRetry: onRetry !== undefined,\n      submit,\n      stop,\n      retry,\n    }),\n    [status, name, failure, onStop, onRetry, submit, stop, retry],\n  );\n\n  return (\n    <ChatContext.Provider value={context}>\n      <div\n        role=\"region\"\n        aria-label={name}\n        {...rest}\n        style={mergeStyles(recipeStyle(cn(CHAT_ROOT, className), theme), style)}\n      >\n        {children}\n      </div>\n    </ChatContext.Provider>\n  );\n};\n\nexport interface ChatHeaderProps extends Omit<Props, \"children\" | \"title\"> {\n  readonly title?: string;\n\n  readonly description?: string;\n\n  readonly actions?: ReactNode;\n\n  readonly className?: string;\n}\n\nconst STATUS_COPY: Readonly<Record<Machine.ChatStatus, string>> = {\n  idle: \"Ready\",\n  submitting: \"Sending…\",\n  streaming: \"Replying…\",\n  error: \"The reply failed.\",\n};\n\nexport const ChatHeader = ({\n  title,\n  description,\n  actions,\n  className,\n  style,\n  ...rest\n}: ChatHeaderProps) => {\n  const theme = useTheme();\n  const { label, status, error, canRetry, retry } = useChat();\n  const name = accessibleName(textOf(title)) ?? label;\n  const failed = status === \"error\";\n  const line = failed\n    ? error\n    : (accessibleName(textOf(description)) ?? STATUS_COPY[status]);\n\n  return (\n    <div\n      {...rest}\n      style={mergeStyles(\n        mergeStyles(recipeStyle(cn(CHAT_HEADER, className), theme), {\n          flexShrink: 0,\n        }),\n        style,\n      )}\n    >\n      <div style={recipeStyle(CHAT_HEADER_COPY, theme)}>\n        <text style={recipeStyle(CHAT_HEADER_TITLE, theme)}>{name}</text>\n        <text\n          testId={failed ? \"chat-error\" : \"chat-status\"}\n          role=\"status\"\n          aria-description={failed ? error : undefined}\n          style={mergeStyles(recipeStyle(CHAT_HEADER_STATUS, theme), {\n            ...(failed\n              ? { color: statusTone(theme, \"destructive\").foreground }\n              : {}),\n\n            width: \"100%\",\n            height: 16,\n            overflow: \"hidden\",\n            whiteSpace: \"nowrap\",\n            textOverflow: \"ellipsis\",\n          })}\n        >\n          {line}\n        </text>\n      </div>\n      <div style={recipeStyle(CHAT_HEADER_ACTIONS, theme)}>\n        {actions}\n        {failed && canRetry ? (\n          <Button testId=\"chat-error-retry\" size=\"sm\" onClick={retry}>\n            Retry\n          </Button>\n        ) : null}\n      </div>\n    </div>\n  );\n};\n\nexport interface ChatMessagesProps extends Omit<Props, \"children\"> {\n  readonly empty?: ReactNode;\n\n  readonly emptyTitle?: string;\n\n  readonly emptyDescription?: string;\n\n  readonly className?: string;\n  readonly children?: PartChildren;\n}\n\nexport const ChatMessages = ({\n  empty,\n  emptyTitle,\n  emptyDescription,\n  className,\n  children,\n  style,\n  ...rest\n}: ChatMessagesProps) => {\n  const theme = useTheme();\n  const { status, label } = useChat();\n  const rows = Children.toArray(children);\n  const emptyConversation = rows.length === 0;\n\n  const title = spokenSentence(textOf(emptyTitle), DEFAULT_EMPTY_TITLE);\n  const description = spokenSentence(\n    textOf(emptyDescription),\n    DEFAULT_EMPTY_DESCRIPTION,\n  );\n\n  return (\n    <MessageScroller\n      label={label}\n      streaming={Machine.isChatBusy(status)}\n      style={{ flexGrow: 1, flexBasis: 0, flexShrink: 1, minHeight: 0 }}\n    >\n      <MessageScrollerViewport\n        {...rest}\n        testId={rest.testId ?? \"chat-messages\"}\n        style={mergeStyles(\n          recipeStyle(cn(CHAT_MESSAGES, className), theme),\n          style,\n        )}\n      >\n        <MessageScrollerContent>\n          {emptyConversation ? (\n            <div style={recipeStyle(CHAT_EMPTY, theme)} testId=\"chat-empty\">\n              {empty ?? (\n                <>\n                  <text style={recipeStyle(CHAT_EMPTY_TITLE, theme)}>\n                    {title}\n                  </text>\n                  <text style={recipeStyle(CHAT_EMPTY_DESCRIPTION, theme)}>\n                    {description}\n                  </text>\n                </>\n              )}\n            </div>\n          ) : (\n            rows.map((child, index) =>\n              isValidElement(child) && child.type === MessageScrollerItem ? (\n                child\n              ) : (\n                <MessageScrollerItem key={index}>{child}</MessageScrollerItem>\n              ),\n            )\n          )}\n        </MessageScrollerContent>\n      </MessageScrollerViewport>\n      <MessageScrollerJumpToLatest testId=\"chat-jump\" />\n    </MessageScroller>\n  );\n};\n\nexport interface ChatComposerState {\n  readonly status: Machine.ChatStatus;\n  readonly busy: boolean;\n  readonly empty: boolean;\n  readonly disabled: boolean;\n}\n\nexport interface ChatComposerProps extends Omit<\n  Props,\n  \"children\" | \"onSubmit\" | \"style\"\n> {\n  readonly value?: string;\n\n  readonly defaultValue?: string;\n\n  readonly onValueChange?: (value: string) => void;\n\n  readonly label?: string;\n\n  readonly placeholder?: string;\n\n  readonly submitLabel?: string;\n\n  readonly stopLabel?: string;\n\n  readonly hint?: string;\n\n  readonly attachments?: ReactNode;\n\n  readonly disabled?: boolean;\n\n  readonly className?: string;\n\n  readonly style?: StateStyle<ChatComposerState>;\n}\n\nexport const ChatComposer = ({\n  value: valueProp,\n  defaultValue = \"\",\n  onValueChange,\n  label,\n  placeholder = \"Send a message…\",\n  submitLabel,\n  stopLabel,\n  hint,\n  attachments,\n  disabled = false,\n  className,\n  style,\n  onKeyDown,\n  ...rest\n}: ChatComposerProps) => {\n  const theme = useTheme();\n  const { status, canStop, submit, stop } = useChat();\n\n  const [draft, setDraft] = useControllableState({\n    value: valueProp === undefined ? undefined : textOf(valueProp),\n    defaultValue: textOf(defaultValue),\n    onChange: onValueChange,\n  });\n\n  const name = accessibleName(textOf(label)) ?? DEFAULT_COMPOSER_LABEL;\n  const submitWord =\n    accessibleName(textOf(submitLabel)) ?? DEFAULT_SUBMIT_LABEL;\n  const stopWord = accessibleName(textOf(stopLabel)) ?? DEFAULT_STOP_LABEL;\n  const hintCopy = visibleCopy(textOf(hint));\n  const busy = Machine.isChatBusy(status);\n  const empty = draft.trim() === \"\";\n  const state: ChatComposerState = { status, busy, empty, disabled };\n\n  const canSend = !empty && !disabled && Machine.canChatSubmit(status);\n\n  const send = (): void => {\n    if (!canSend) return;\n    submit(draft);\n    setDraft(\"\");\n  };\n\n  const fieldStyle = mergeStyles(recipeStyle(CHAT_COMPOSER_FIELD, theme), {\n    ...(disabled\n      ? { color: theme.colors.disabledForeground }\n      : keyboardRing(theme)),\n    ...selectionWash(theme),\n    ...keepContent,\n    minHeight: theme.spacing[\"2xl\"],\n  });\n  const placeholderHint = fieldHint({ placeholder, disabled }, theme, draft, {\n    origin: \"host\",\n    align: \"top\",\n    style: fieldStyle,\n  });\n\n  return (\n    <div\n      {...rest}\n      style={mergeStyles(\n        mergeStyles(recipeStyle(cn(CHAT_COMPOSER, className), theme), {\n          flexShrink: 0,\n        }),\n        resolveStyle(style, state),\n      )}\n    >\n      {attachments === undefined ? null : (\n        <div style={recipeStyle(CHAT_COMPOSER_ATTACHMENTS, theme)}>\n          {attachments}\n        </div>\n      )}\n      <div\n        testId=\"chat-composer-row\"\n        style={recipeStyle(CHAT_COMPOSER_ROW, theme)}\n      >\n        <div style={hintHost({ flexGrow: 1 })}>\n          <textarea\n            testId=\"chat-field\"\n            aria-label={name}\n            aria-description={fieldHelp({ placeholder }, draft)}\n            theme={nativeTextTheme(theme, \"background\")}\n            value={draft}\n            readOnly={disabled}\n            tabIndex={disabled ? -1 : 0}\n            minRows={1}\n            maxRows={6}\n            onChange={(event) => setDraft(event.value ?? \"\")}\n            onSubmit={send}\n            onKeyDown={(event: KeyEvent) => {\n              onKeyDown?.(event);\n\n              if (busy && canStop && isDismiss(event)) stop();\n            }}\n            style={fieldStyle}\n          />\n          {Option.match(placeholderHint, {\n            onNone: () => null,\n            onSome: (painted) => renderFieldHint(painted, \"chat-field\"),\n          })}\n        </div>\n        {busy && canStop ? (\n          <Button\n            testId=\"chat-stop\"\n            size=\"sm\"\n            variant=\"secondary\"\n            onClick={stop}\n          >\n            {stopWord}\n          </Button>\n        ) : (\n          <Button\n            testId=\"chat-send\"\n            size=\"sm\"\n            variant=\"primary\"\n            disabled={!canSend}\n            aria-label={submitWord}\n            onClick={send}\n          >\n            <Icon\n              name=\"send\"\n              size=\"sm\"\n              color={actionOnColor(theme, \"primary\")}\n            />\n          </Button>\n        )}\n      </div>\n      {hintCopy === undefined ? null : (\n        <text style={recipeStyle(CHAT_COMPOSER_HINT, theme)}>{hintCopy}</text>\n      )}\n    </div>\n  );\n};\n\nconst chatParts: ReadonlyArray<PartComponent> = [\n  ChatRoot,\n  ChatHeader,\n  ChatMessages,\n  ChatComposer,\n];\n\nconst chatRoot: PartsContract = {\n  parent: \"ChatRoot\",\n  family: chatParts,\n  accepts: [ChatHeader, ChatMessages, ChatComposer],\n  hint: \"Put ChatHeader, ChatMessages and ChatComposer directly inside ChatRoot.\",\n};\n\nexport const chatMeta = {\n  name: \"Chat\",\n  tier: 2,\n  status: \"done\",\n  gpuixSupport: \"partial\",\n  limitations: [\n    \"the transport is the consumer's: the component emits submit/stop/retry intents and never talks to a model\",\n    \"Enter reaches the textarea's onSubmit listener; a textarea without it consumes Enter as a newline and no keydown arrives (measured in a window)\",\n    \"Shift+Enter inserts a newline and reaches no handler at all (measured), so a composer cannot distinguish it from a paste\",\n    \"no aria-live, so a new row is painted but not announced; the header's status line is a marker, not a live region\",\n    \"the failure copy is one line in the fixed header (full sentence in aria-description), not a row between the viewport and the composer: a row there took height from the transcript and moved the composer when it appeared\",\n    \"the jump control owns a reserved rail under the viewport, because position: absolute resolves against the mount parent in 0.11.0\",\n  ],\n  docs: \"docs/chat.mdx\",\n  example: \"examples/chat.example.tsx\",\n  testFile: \"src/behaviour/chat.test.tsx\",\n} as const;\n"
    },
    {
      "path": "packages/components/src/chat/logic.ts",
      "target": "@ui/gpuixui/components/chat/logic.ts",
      "type": "registry:lib",
      "content": "import { Match } from \"effect\";\nimport * as Option from \"effect/Option\";\nimport * as Schema from \"effect/Schema\";\n\nexport const ChatStatus = Schema.Literals([\n  \"idle\",\n  \"submitting\",\n  \"streaming\",\n  \"error\",\n]);\n\nexport type ChatStatus = typeof ChatStatus.Type;\n\nexport const isChatStatus = Schema.is(ChatStatus);\n\nexport type ChatCommand =\n  | \"submit\"\n  | \"retry\"\n  | \"stop\"\n  | \"begin\"\n  | \"delta\"\n  | \"complete\"\n  | \"fail\";\n\nexport type ChatEmit = \"submit\" | \"stop\" | \"retry\";\n\nexport interface ChatMove {\n  readonly status: ChatStatus;\n\n  readonly emit: Option.Option<ChatEmit>;\n}\n\nconst noIntent = (status: ChatStatus): ChatMove => ({\n  status,\n  emit: Option.none(),\n});\n\nconst calls = (status: ChatStatus, emit: ChatEmit): ChatMove => ({\n  status,\n  emit: Option.some(emit),\n});\n\nexport const isChatBusy = (status: ChatStatus): boolean =>\n  status === \"submitting\" || status === \"streaming\";\n\nexport const canChatSubmit = (status: ChatStatus): boolean =>\n  !isChatBusy(status);\n\nexport const chatMove = (status: ChatStatus, command: ChatCommand): ChatMove =>\n  Match.value(command).pipe(\n    Match.when(\"submit\", () =>\n      canChatSubmit(status) ? calls(\"submitting\", \"submit\") : noIntent(status),\n    ),\n    Match.when(\"retry\", () =>\n      status === \"error\" ? calls(\"submitting\", \"retry\") : noIntent(status),\n    ),\n    Match.when(\"stop\", () =>\n      isChatBusy(status) ? calls(\"idle\", \"stop\") : noIntent(status),\n    ),\n    Match.when(\"begin\", () =>\n      status === \"submitting\" ? noIntent(\"streaming\") : noIntent(status),\n    ),\n    Match.when(\"delta\", () =>\n      isChatBusy(status) ? noIntent(\"streaming\") : noIntent(status),\n    ),\n    Match.when(\"complete\", () =>\n      isChatBusy(status) ? noIntent(\"idle\") : noIntent(status),\n    ),\n    Match.when(\"fail\", () =>\n      isChatBusy(status) ? noIntent(\"error\") : noIntent(status),\n    ),\n    Match.exhaustive,\n  );\n"
    },
    {
      "path": "packages/components/src/internal/button.tsx",
      "target": "@ui/gpuixui/components/internal/button.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { Props } from \"@gpuix/react\";\nimport type { ReactNode } from \"react\";\nimport {\n  Button as PrimitiveButton,\n  IconButton as PrimitiveIconButton,\n  actionOnColor as primitiveActionOnColor,\n  type ButtonVariant as PrimitiveVariant,\n} from \"../../primitives/button.js\";\nimport { cn } from \"../../tw/index.js\";\nimport * as Schema from \"effect/Schema\";\nimport type { Theme } from \"./theme.js\";\n\nconst ButtonVariants = Schema.Literals([\n  \"primary\",\n  \"secondary\",\n  \"ghost\",\n  \"danger\",\n  \"link\",\n]);\n\nexport type ButtonVariant = typeof ButtonVariants.Type;\n\nexport type ButtonSize = \"sm\" | \"md\" | \"lg\";\n\nexport interface ButtonProps extends Props {\n  readonly variant?: ButtonVariant;\n\n  readonly size?: ButtonSize;\n\n  readonly disabled?: boolean;\n\n  readonly loading?: boolean;\n\n  readonly loadingLabel?: string;\n\n  readonly reducedMotion?: boolean;\n\n  readonly leading?: ReactNode;\n\n  readonly trailing?: ReactNode;\n}\n\nexport interface IconButtonProps extends Props {\n  readonly label: string;\n\n  readonly size?: ButtonSize;\n  readonly disabled?: boolean;\n}\n\ninterface Look {\n  readonly variant: PrimitiveVariant;\n  readonly className: string;\n}\n\nconst LOOKS: Readonly<Record<ButtonVariant, Look>> = {\n  primary: { variant: \"primary\", className: \"\" },\n  secondary: { variant: \"secondary\", className: \"border border-strong\" },\n  ghost: { variant: \"ghost\", className: \"\" },\n  danger: { variant: \"destructive\", className: \"\" },\n\n  link: { variant: \"link\", className: \"underline underline-offset-4\" },\n};\n\nconst lookOf = (variant: unknown): Look =>\n  LOOKS[Schema.is(ButtonVariants)(variant) ? variant : \"secondary\"];\n\nexport const actionOnColor = (theme: Theme, variant: ButtonVariant): string =>\n  primitiveActionOnColor(theme, lookOf(variant).variant);\n\nexport const Button = ({\n  variant = \"secondary\",\n  size = \"md\",\n  disabled = false,\n  onClick,\n  style,\n  \"aria-label\": ariaLabel,\n  ...rest\n}: ButtonProps) => {\n  const look = lookOf(variant);\n  return (\n    <PrimitiveButton\n      {...rest}\n      variant={look.variant}\n      size={size}\n      disabled={disabled}\n      label={ariaLabel}\n      className={cn(look.className)}\n      onActivate={onClick}\n      style={style}\n    />\n  );\n};\n\nexport const IconButton = ({\n  label,\n  size = \"md\",\n  disabled = false,\n  onClick,\n  style,\n  children,\n  ...rest\n}: IconButtonProps) => (\n  <PrimitiveIconButton\n    {...rest}\n    icon={children}\n    variant=\"ghost\"\n    size={size}\n    disabled={disabled}\n    label={label}\n    className=\"text-muted-foreground hover:text-foreground\"\n    onActivate={onClick}\n    style={style}\n  />\n);\n"
    },
    {
      "path": "packages/components/src/internal/controllable.ts",
      "target": "@ui/gpuixui/components/internal/controllable.ts",
      "type": "registry:lib",
      "content": "import { useCallback, useState } from \"react\";\n\nexport type ControllableState<Value> = readonly [Value, (next: Value) => void];\n\nexport interface ControllableStateOptions<Value> {\n  readonly value: Value | undefined;\n\n  readonly defaultValue: Value;\n\n  readonly onChange?: (next: Value) => void;\n}\n\nexport const useControllableState = <Value>({\n  value,\n  defaultValue,\n  onChange,\n}: ControllableStateOptions<Value>): ControllableState<Value> => {\n  const [internal, setInternal] = useState(defaultValue);\n  const controlled = value !== undefined;\n\n  const setValue = useCallback(\n    (next: Value) => {\n      if (!controlled) setInternal(next);\n      if (value !== next) onChange?.(next);\n    },\n    [controlled, value, onChange],\n  );\n\n  return [controlled ? value : internal, setValue];\n};\n"
    },
    {
      "path": "packages/components/src/internal/key.ts",
      "target": "@ui/gpuixui/components/internal/key.ts",
      "type": "registry:lib",
      "content": "import type { EventPayload, KeyEvent, Props } from \"@gpuix/react\";\nimport * as Option from \"effect/Option\";\n\nexport type KeyPhase = \"down\" | \"up\";\n\nexport const isActivation = (event: EventPayload, phase: KeyPhase): boolean => {\n  if (event.isHeld === true) return false;\n  if (event.key === \"enter\") return phase === \"down\";\n  return event.key === \"space\" && phase === \"up\";\n};\n\nexport const swallowsDefault = (\n  event: EventPayload,\n  phase: KeyPhase,\n): boolean => event.key === \"space\" && phase === \"down\";\n\nexport const activationHandlers = (\n  activate: (event: KeyEvent) => void,\n  observe: Pick<Props, \"onKeyDown\" | \"onKeyUp\"> = {},\n): Pick<Props, \"onKeyDown\" | \"onKeyUp\"> => ({\n  onKeyDown: (event: KeyEvent) => {\n    if (swallowsDefault(event, \"down\")) event.preventDefault();\n    if (isActivation(event, \"down\")) activate(event);\n    observe.onKeyDown?.(event);\n  },\n  onKeyUp: (event: KeyEvent) => {\n    if (isActivation(event, \"up\")) activate(event);\n    observe.onKeyUp?.(event);\n  },\n});\n\nexport const isEnter = (event: EventPayload): boolean => event.key === \"enter\";\n\nexport const isDown = (event: EventPayload): boolean =>\n  event.key === \"down\" || (event.key === \"n\" && event.modifiers?.ctrl === true);\n\nexport const isUp = (event: EventPayload): boolean =>\n  event.key === \"up\" || (event.key === \"p\" && event.modifiers?.ctrl === true);\n\nexport const isRight = (event: EventPayload): boolean => event.key === \"right\";\n\nexport const isLeft = (event: EventPayload): boolean => event.key === \"left\";\n\nexport const isFirst = (event: EventPayload): boolean => event.key === \"home\";\n\nexport const isLast = (event: EventPayload): boolean => event.key === \"end\";\n\nexport const isPageUp = (event: EventPayload): boolean =>\n  event.key === \"pageup\";\n\nexport const isPageDown = (event: EventPayload): boolean =>\n  event.key === \"pagedown\";\n\nexport const isDismiss = (event: EventPayload): boolean =>\n  event.key === \"escape\";\n\nexport const isTab = (event: EventPayload): boolean => event.key === \"tab\";\n\nexport const isSelectAll = (event: EventPayload): boolean =>\n  event.key === \"a\" &&\n  (event.modifiers?.cmd === true || event.modifiers?.ctrl === true);\n\nexport const isMenuKey = (event: EventPayload): boolean =>\n  (event.key === \"f10\" && event.modifiers?.shift === true) ||\n  event.key === \"contextmenu\";\n\nexport const axisDirection = (\n  event: EventPayload,\n  orientation: \"horizontal\" | \"vertical\",\n): Option.Option<1 | -1> => {\n  if (event.key === \"right\") return Option.some(1);\n  if (event.key === \"left\") return Option.some(-1);\n  if (orientation === \"vertical\") {\n    if (isDown(event)) return Option.some(1);\n    if (isUp(event)) return Option.some(-1);\n  }\n  return Option.none();\n};\n"
    },
    {
      "path": "packages/components/src/internal/local-state.ts",
      "target": "@ui/gpuixui/components/internal/local-state.ts",
      "type": "registry:lib",
      "content": "import { Effect } from \"effect\";\nimport * as Arr from \"effect/Array\";\nimport * as Duration from \"effect/Duration\";\nimport * as Fiber from \"effect/Fiber\";\nimport * as Option from \"effect/Option\";\nimport * as Ref from \"effect/Ref\";\nimport * as SubscriptionRef from \"effect/SubscriptionRef\";\nimport { useEffect, useState, useSyncExternalStore } from \"react\";\n\nexport interface LocalState<A> {\n  readonly state: SubscriptionRef.SubscriptionRef<A>;\n\n  readonly read: () => A;\n\n  readonly subscribe: (listener: () => void) => () => void;\n\n  readonly write: (next: A) => Effect.Effect<void>;\n\n  readonly after: (\n    millis: number,\n    effect: Effect.Effect<void>,\n  ) => Effect.Effect<void>;\n\n  readonly cancel: Effect.Effect<void>;\n\n  readonly running: () => boolean;\n}\n\nexport const makeLocalState = <A>(initial: A): Effect.Effect<LocalState<A>> =>\n  Effect.gen(function* () {\n    const state = yield* SubscriptionRef.make(initial);\n    const timer = yield* Ref.make(Option.none<Fiber.Fiber<void>>());\n    const listeners = yield* Ref.make<ReadonlyArray<() => void>>(Arr.empty());\n\n    const write = Effect.fnUntraced(function* (next: A) {\n      yield* SubscriptionRef.set(state, next);\n      const subscribers = yield* Ref.get(listeners);\n      yield* Effect.sync(() => {\n        Arr.forEach(subscribers, (listener) => listener());\n      });\n    });\n\n    const cancel = Effect.gen(function* () {\n      const held = yield* Ref.getAndSet(timer, Option.none());\n      yield* Option.match(held, {\n        onNone: () => Effect.void,\n        onSome: Fiber.interrupt,\n      });\n    });\n\n    const after = Effect.fnUntraced(function* (\n      millis: number,\n      effect: Effect.Effect<void>,\n    ) {\n      yield* cancel;\n      const fiber = yield* Effect.forkDetach(\n        Effect.sleep(Duration.millis(millis)).pipe(Effect.andThen(effect)),\n      );\n      yield* Ref.set(timer, Option.some(fiber));\n    });\n\n    const running = (): boolean =>\n      Option.exists(\n        Ref.getUnsafe(timer),\n        (fiber) => fiber.pollUnsafe() === undefined,\n      );\n\n    const subscribe = (listener: () => void): (() => void) => {\n      Effect.runSync(\n        Ref.update(listeners, (current) => Arr.append(current, listener)),\n      );\n      return () => {\n        Effect.runSync(\n          Ref.update(listeners, (current) =>\n            Arr.filter(current, (held) => held !== listener),\n          ),\n        );\n      };\n    };\n\n    return {\n      state,\n\n      read: () => SubscriptionRef.getUnsafe(state),\n      subscribe,\n      write,\n      after,\n      cancel,\n      running,\n    };\n  });\n\ninterface Teardown {\n  readonly local: { readonly cancel: Effect.Effect<void> };\n}\n\nexport const useLocalStore = <Store extends Teardown>(\n  make: Effect.Effect<Store>,\n): Store => {\n  const [store] = useState(() => Effect.runSync(make));\n  useEffect(() => () => send(store.local.cancel), [store]);\n  return store;\n};\n\nexport const send = (command: Effect.Effect<void>): void => {\n  Effect.runFork(command);\n};\n\nexport const useLocalState = <A>(local: LocalState<A>): A =>\n  useSyncExternalStore(local.subscribe, local.read, local.read);\n\nexport const useLocalSlice = <A, B>(\n  local: LocalState<A>,\n  select: (state: A) => B,\n): B =>\n  useSyncExternalStore(\n    local.subscribe,\n    () => select(local.read()),\n    () => select(local.read()),\n  );\n"
    },
    {
      "path": "packages/components/src/internal/parts.ts",
      "target": "@ui/gpuixui/components/internal/parts.ts",
      "type": "registry:lib",
      "content": "import * as Arr from \"effect/Array\";\nimport * as P from \"effect/Predicate\";\nimport {\n  Children,\n  Fragment,\n  isValidElement,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\n\nexport type PartComponent = (props: never) => ReactNode;\n\nexport type PartChildren =\n  | ReactElement<unknown>\n  | boolean\n  | null\n  | undefined\n  | ReadonlyArray<PartChildren>;\n\nexport interface PartsContract {\n  readonly parent: string;\n\n  readonly family: ReadonlyArray<PartComponent>;\n\n  readonly accepts: ReadonlyArray<PartComponent>;\n\n  readonly hint?: string;\n}\n\nconst componentName = (type: unknown): string =>\n  P.isString(type)\n    ? type\n    : P.isFunction(type)\n      ? type.name || \"anonymous\"\n      : \"element\";\n\nconst fragmentChildren = (element: ReactElement<unknown>): ReactNode =>\n  hasChildren(element.props) ? element.props.children : undefined;\n\nconst hasChildren = (\n  value: unknown,\n): value is { readonly children?: ReactNode } =>\n  P.isObject(value) && \"children\" in value;\n\nconst leaves = (children: ReactNode): ReadonlyArray<unknown> =>\n  Arr.flatMap(Children.toArray(children), (child) =>\n    isValidElement<unknown>(child) && child.type === Fragment\n      ? leaves(fragmentChildren(child))\n      : [child],\n  );\n\nexport const assertParts = (\n  contract: PartsContract,\n  children: ReactNode,\n): void => {\n  Arr.forEach(leaves(children), (leaf) => {\n    if (!isValidElement(leaf)) return;\n    if (Arr.some(contract.accepts, (part) => part === leaf.type)) return;\n    if (!Arr.some(contract.family, (part) => part === leaf.type)) return;\n\n    throw new Error(\n      `${componentName(leaf.type)} cannot be used inside ${contract.parent}.` +\n        (contract.hint === undefined ? \"\" : ` ${contract.hint}`),\n    );\n  });\n};\n"
    },
    {
      "path": "packages/components/src/internal/recipe.ts",
      "target": "@ui/gpuixui/components/internal/recipe.ts",
      "type": "registry:lib",
      "content": "import type { StyleDesc } from \"@gpuix/react\";\nimport { createRecipeCache } from \"../../tw/index.js\";\nimport type { Theme } from \"./theme.js\";\n\nexport const recipeStyle: (recipe: string, theme: Theme) => StyleDesc =\n  createRecipeCache(512);\n"
    },
    {
      "path": "packages/components/src/internal/ref.ts",
      "target": "@ui/gpuixui/components/internal/ref.ts",
      "type": "registry:lib",
      "content": "import type { Ref, RefCallback } from \"react\";\nimport { useCallback } from \"react\";\n\nconst assign = <T>(ref: Ref<T> | undefined, value: T): void => {\n  if (ref === null || ref === undefined) return;\n  if (typeof ref === \"function\") {\n    ref(value);\n    return;\n  }\n  ref.current = value;\n};\n\nexport const useMergedRef = <T>(\n  internal: Ref<T>,\n  caller: Ref<T> | undefined,\n): RefCallback<T> =>\n  useCallback(\n    (value: T) => {\n      assign(internal, value);\n      assign(caller, value);\n    },\n    [internal, caller],\n  );\n"
    },
    {
      "path": "packages/components/src/internal/text.ts",
      "target": "@ui/gpuixui/components/internal/text.ts",
      "type": "registry:lib",
      "content": "import { pipe } from \"effect\";\nimport * as Option from \"effect/Option\";\nimport * as Str from \"effect/String\";\n\nexport const textOf = (value: unknown): string =>\n  typeof value === \"string\" ? value : \"\";\n\nexport const nameOf = (value: unknown): string | undefined =>\n  pipe(\n    Option.some(textOf(value)),\n    Option.map(Str.trim),\n    Option.filter(Str.isNonEmpty),\n    Option.getOrUndefined,\n  );\n"
    },
    {
      "path": "packages/components/src/internal/theme.ts",
      "target": "@ui/gpuixui/components/internal/theme.ts",
      "type": "registry:lib",
      "content": "import type { StyleDesc } from \"@gpuix/react\";\nimport * as Schema from \"effect/Schema\";\nimport {\n  alpha,\n  avatarPixels,\n  avatarSize,\n  brandTextOn,\n  controlFocusRing,\n  controlHeight,\n  controlPaddingX,\n  currentPlatform,\n  darkTheme,\n  disabledAction,\n  disabledOpacity,\n  elevationShadow,\n  filledHover,\n  filledPressed,\n  findHighlight,\n  focusRing,\n  keyboardFocusRing,\n  lightTheme,\n  nativeTextTheme,\n  pointerWash,\n  selectionOn,\n  statusTone,\n  syntaxColors,\n  targetSize,\n  textStyle,\n  type BoxShadow,\n  type ElevationLevel,\n  type FindHighlightSource,\n  type NativeTextFill,\n  type PlatformName,\n  type PointerWashSurface,\n  type StatusTone,\n  type SyntaxColors,\n  type SyntaxRole,\n  type TextRole,\n  type Theme,\n  type ThemeColors,\n} from \"../../tokens/index.js\";\n\nexport {\n  alpha,\n  avatarPixels,\n  avatarSize,\n  controlFocusRing,\n  controlHeight,\n  controlPaddingX,\n  currentPlatform,\n  darkTheme,\n  disabledAction,\n  disabledOpacity,\n  elevationShadow,\n  filledHover,\n  filledPressed,\n  findHighlight,\n  focusRing,\n  keyboardFocusRing,\n  lightTheme,\n  nativeTextTheme,\n  statusTone,\n  syntaxColors,\n  targetSize,\n  textStyle,\n};\nexport type {\n  BoxShadow,\n  ElevationLevel,\n  FindHighlightSource,\n  NativeTextFill,\n  PlatformName,\n  StatusTone,\n  SyntaxColors,\n  SyntaxRole,\n  TextRole,\n  Theme,\n};\nexport { ThemeProvider, useTheme } from \"../../tokens/react.js\";\n\nexport const keyboardRing = (theme: Theme, invalid = false): StyleDesc =>\n  controlFocusRing(theme, { invalid }) ?? {};\n\nexport interface ControlRingState {\n  readonly disabled?: boolean;\n\n  readonly invalid?: boolean;\n}\n\nexport const controlRing = (\n  theme: Theme,\n  state: ControlRingState = {},\n): StyleDesc => controlFocusRing(theme, state) ?? {};\n\nexport const pressWash = (theme: Theme, replaced: PointerWashSurface): string =>\n  pointerWash(theme, \"active\", replaced) ?? theme.colors.active;\n\nexport const selectionWash = (theme: Theme): StyleDesc => ({\n  selectionColor: theme.colors.selection,\n});\n\nexport const keepContent: StyleDesc = { keyboardFocusDim: false };\n\nexport const disabledFade = (\n  disabled: boolean,\n): { readonly opacity: number } | undefined =>\n  disabled ? { opacity: disabledOpacity } : undefined;\n\nconst Tones = Schema.Literals([\"info\", \"success\", \"warning\", \"danger\"]);\n\nexport type Tone = typeof Tones.Type;\n\nexport const toneOf = (value: unknown): Tone =>\n  Schema.is(Tones)(value) ? value : \"info\";\n\nexport interface ToneStyle {\n  readonly fg: string;\n  readonly bg: string;\n  readonly border: string;\n}\n\nconst TOKEN_TONE: Readonly<Record<Tone, StatusTone>> = {\n  info: \"info\",\n  success: \"success\",\n  warning: \"warning\",\n  danger: \"destructive\",\n};\n\nexport const tone = (theme: Theme, value: Tone): ToneStyle => {\n  const { background, border, foreground } = statusTone(\n    theme,\n    TOKEN_TONE[value],\n  );\n  return { bg: background, border, fg: foreground };\n};\n\nconst softRoles: Readonly<Record<Tone, keyof ThemeColors>> = {\n  info: \"infoSoft\",\n  success: \"successSoft\",\n  warning: \"warningSoft\",\n  danger: \"destructiveSoft\",\n};\n\nexport const messageLink = (theme: Theme, value: Tone): string =>\n  brandTextOn(theme, softRoles[value]);\n\nexport const messageSelection = (theme: Theme, value: Tone): string =>\n  selectionOn(theme, softRoles[value]);\n\nexport const toneSurface = (theme: Theme, value: Tone): StyleDesc => {\n  const { bg, border, fg } = tone(theme, value);\n  return {\n    backgroundColor: bg,\n    color: fg,\n    borderWidth: theme.borderWidth.thin,\n    borderColor: border,\n  };\n};\n"
    },
    {
      "path": "packages/components/src/message-scroller.tsx",
      "target": "@ui/gpuixui/components/message-scroller.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type {\n  EventPayload,\n  Props,\n  PublicInstance,\n  StyleDesc,\n} from \"@gpuix/react\";\nimport { useGpuix } from \"@gpuix/react\";\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport {\n  cn,\n  MESSAGE_SCROLLER_CONTENT,\n  MESSAGE_SCROLLER_ITEM,\n  MESSAGE_SCROLLER_JUMP,\n  MESSAGE_SCROLLER_ROOT,\n  MESSAGE_SCROLLER_VIEWPORT,\n} from \"../tw/index.js\";\nimport {\n  Children,\n  createContext,\n  useContext,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  type ReactNode,\n  type Ref,\n} from \"react\";\nimport { Button } from \"./internal/button.js\";\nimport { send, useLocalState, useLocalStore } from \"./internal/local-state.js\";\nimport {\n  assertParts,\n  type PartChildren,\n  type PartComponent,\n  type PartsContract,\n} from \"./internal/parts.js\";\nimport { recipeStyle } from \"./internal/recipe.js\";\nimport { useMergedRef } from \"./internal/ref.js\";\nimport { useTheme } from \"./internal/theme.js\";\nimport {\n  makeMessageScrollerStore,\n  type MessageScrollerState,\n} from \"./message-scroller/store.js\";\n\ninterface MessageScrollerContextValue {\n  readonly threshold: number;\n  readonly peek: number;\n\n  readonly ready: { current: boolean };\n\n  readonly pinned: { current: boolean };\n  readonly observe: (distanceFromEnd: number) => void;\n  readonly arrived: () => void;\n  readonly jump: () => void;\n\n  readonly scrollToEnd: { current: () => void };\n\n  readonly alignTo: { current: (instance: PublicInstance) => void };\n}\n\nconst MessageScrollerContext = createContext<\n  MessageScrollerContextValue | undefined\n>(undefined);\n\nconst MessageScrollerStateContext = createContext<\n  MessageScrollerState | undefined\n>(undefined);\n\nconst useMessageScroller = (): MessageScrollerContextValue => {\n  const value = useContext(MessageScrollerContext);\n  if (value === undefined) {\n    throw new Error(\n      \"MessageScroller parts must be used inside MessageScroller\",\n    );\n  }\n  return value;\n};\n\nconst useMessageScrollerState = (): MessageScrollerState => {\n  const value = useContext(MessageScrollerStateContext);\n  if (value === undefined) {\n    throw new Error(\n      \"MessageScroller parts must be used inside MessageScroller\",\n    );\n  }\n  return value;\n};\n\nconst useFollowAppendedRows = (children: PartChildren): void => {\n  const scroller = useMessageScroller();\n  const rows = Children.count(children);\n  useLayoutEffect(() => {\n    if (scroller.pinned.current && scroller.ready.current) {\n      scroller.scrollToEnd.current();\n    }\n  }, [rows, scroller]);\n};\n\nexport interface MessageScrollerProps extends Omit<Props, \"children\"> {\n  readonly label: string;\n\n  readonly threshold?: number;\n\n  readonly previousItemPeek?: number;\n\n  readonly pinned?: boolean;\n  readonly defaultPinned?: boolean;\n  readonly onPinnedChange?: (pinned: boolean) => void;\n\n  readonly streaming?: boolean;\n\n  readonly className?: string;\n  readonly children?: PartChildren;\n}\n\nexport const MessageScroller = ({\n  label,\n  threshold: thresholdProp,\n  previousItemPeek,\n  pinned: pinnedProp,\n  defaultPinned = true,\n  onPinnedChange,\n  streaming = false,\n  children,\n  className,\n  style,\n  ...rest\n}: MessageScrollerProps) => {\n  const theme = useTheme();\n  const store = useLocalStore(makeMessageScrollerStore(defaultPinned));\n  const state = useLocalState(store.local);\n  const threshold = thresholdProp ?? theme.spacing[\"2xl\"];\n  const peek = previousItemPeek ?? theme.spacing[\"3xl\"];\n  const ready = useRef(false);\n  const pinned = useRef(state.pinned);\n  pinned.current = state.pinned;\n  const scrollToEnd = useRef<() => void>(() => undefined);\n  const alignTo = useRef<(instance: PublicInstance) => void>(() => undefined);\n  assertParts(messageScrollerRoot, children);\n\n  useEffect(() => {\n    send(store.stream(streaming));\n  }, [store, streaming]);\n\n  useEffect(() => {\n    if (pinnedProp === undefined) return;\n    send(store.setPinned(pinnedProp));\n  }, [pinnedProp, store]);\n\n  const observe = useMemo(\n    () => (distanceFromEnd: number) => {\n      const before = store.local.read().pinned;\n      send(store.observe({ distanceFromEnd, threshold }));\n      const after = store.local.read().pinned;\n      if (after !== before) onPinnedChange?.(after);\n    },\n    [onPinnedChange, store, threshold],\n  );\n\n  const arrived = useMemo(\n    () => () => {\n      send(store.arrived);\n    },\n    [store],\n  );\n\n  const jump = useMemo(\n    () => () => {\n      send(store.jump);\n      scrollToEnd.current();\n      onPinnedChange?.(true);\n    },\n    [onPinnedChange, store],\n  );\n\n  const context = useMemo<MessageScrollerContextValue>(\n    () => ({\n      threshold,\n      peek,\n      ready,\n      pinned,\n      observe,\n      arrived,\n      jump,\n      scrollToEnd,\n      alignTo,\n    }),\n    [threshold, peek, observe, arrived, jump],\n  );\n\n  return (\n    <MessageScrollerContext.Provider value={context}>\n      <MessageScrollerStateContext.Provider value={state}>\n        <div\n          {...rest}\n          role=\"group\"\n          aria-label={label}\n          aria-busy={state.streaming}\n          style={mergeStyles(\n            recipeStyle(cn(MESSAGE_SCROLLER_ROOT, className), theme),\n            style,\n          )}\n        >\n          {children}\n        </div>\n      </MessageScrollerStateContext.Provider>\n    </MessageScrollerContext.Provider>\n  );\n};\n\nexport interface MessageScrollerViewportProps extends Omit<Props, \"children\"> {\n  readonly className?: string;\n  readonly children?: PartChildren;\n  readonly ref?: Ref<PublicInstance>;\n}\n\nexport const MessageScrollerViewport = ({\n  children,\n  className,\n  style,\n  ref,\n  onScroll,\n  onLayout,\n  ...rest\n}: MessageScrollerViewportProps) => {\n  const theme = useTheme();\n  const renderer = useGpuix().renderer;\n  const scroller = useMessageScroller();\n  const viewportRef = useRef<PublicInstance | null>(null);\n  const tailRef = useRef<PublicInstance | null>(null);\n  const mergedRef = useMergedRef(viewportRef, ref);\n\n  const programmatic = useRef(false);\n  const lastOffset = useRef(0);\n  assertParts(messageScrollerViewport, children);\n\n  const scrollToEnd = (): void => {\n    const tail = tailRef.current;\n    if (tail === null) return;\n    programmatic.current = true;\n    const viewport = viewportRef.current;\n    if (renderer?.scrollIntoView !== undefined) {\n      renderer.scrollIntoView(tail.id);\n    } else if (viewport !== null && renderer?.scrollTo !== undefined) {\n      renderer.scrollTo(viewport.id, 0, -Number.MAX_SAFE_INTEGER);\n    }\n\n    const offset =\n      viewport === null\n        ? undefined\n        : renderer?.getScrollOffset?.(viewport.id)?.[1];\n    if (offset !== undefined) lastOffset.current = offset;\n  };\n\n  useLayoutEffect(() => {\n    scroller.scrollToEnd.current = scrollToEnd;\n  });\n\n  useEffect(() => {\n    if (scroller.ready.current) return;\n    scroller.ready.current = true;\n    if (scroller.pinned.current) scrollToEnd();\n  }, []);\n\n  useFollowAppendedRows(children);\n\n  const align = (instance: PublicInstance): void => {\n    const viewport = viewportRef.current;\n    if (viewport === null || renderer === null) return;\n    if (\n      renderer.getElementBounds !== undefined &&\n      renderer.getScrollOffset !== undefined &&\n      renderer.scrollTo !== undefined\n    ) {\n      const box = renderer.getElementBounds(viewport.id);\n      const target = renderer.getElementBounds(instance.id);\n      if (box !== null && target !== null) {\n        const offset = renderer.getScrollOffset(viewport.id)?.[1] ?? 0;\n\n        const next = Math.min(0, offset - (target.y - box.y) + scroller.peek);\n        renderer.scrollTo(viewport.id, 0, next);\n        return;\n      }\n    }\n    renderer.scrollIntoView?.(instance.id);\n  };\n\n  useLayoutEffect(() => {\n    scroller.alignTo.current = align;\n  });\n\n  const handleScroll = (event: EventPayload): void => {\n    const viewport = viewportRef.current;\n    const tail = tailRef.current;\n    if (renderer !== null && viewport !== null && tail !== null) {\n      const viewportBounds = renderer.getElementBounds?.(viewport.id);\n      const tailBounds = renderer.getElementBounds?.(tail.id);\n      if (\n        viewportBounds !== undefined &&\n        viewportBounds !== null &&\n        tailBounds !== undefined &&\n        tailBounds !== null\n      ) {\n        const distance =\n          tailBounds.y - (viewportBounds.y + viewportBounds.height);\n        const offset = renderer.getScrollOffset?.(viewport.id)?.[1] ?? 0;\n        const movedUp = offset > lastOffset.current;\n        lastOffset.current = offset;\n        if (programmatic.current) {\n          if (!movedUp && distance > scroller.threshold) return;\n          programmatic.current = false;\n        }\n        scroller.observe(distance);\n      }\n    }\n    onScroll?.(event);\n  };\n\n  const base: StyleDesc = {\n    ...recipeStyle(cn(MESSAGE_SCROLLER_VIEWPORT, className), theme),\n    overflowY: \"scroll\",\n\n    flexGrow: 1,\n    flexShrink: 1,\n    minHeight: 0,\n  };\n\n  return (\n    <div\n      {...rest}\n      ref={mergedRef}\n      onScroll={handleScroll}\n      onLayout={(event) => {\n        if (!scroller.ready.current) {\n          scroller.ready.current = true;\n          if (scroller.pinned.current) scrollToEnd();\n        }\n        onLayout?.(event);\n      }}\n      style={mergeStyles(base, style)}\n    >\n      {children}\n      <div\n        ref={tailRef}\n        onLayout={() => {\n          if (scroller.pinned.current && scroller.ready.current) scrollToEnd();\n        }}\n        style={{ height: 1, flexShrink: 0 }}\n      />\n    </div>\n  );\n};\n\nexport interface MessageScrollerContentProps extends Omit<Props, \"children\"> {\n  readonly className?: string;\n  readonly children?: PartChildren;\n}\n\nexport const MessageScrollerContent = ({\n  children,\n  className,\n  style,\n  ...rest\n}: MessageScrollerContentProps) => {\n  const theme = useTheme();\n  const state = useMessageScrollerState();\n  useFollowAppendedRows(children);\n  assertParts(messageScrollerContent, children);\n  return (\n    <div\n      {...rest}\n      aria-busy={state.streaming}\n      style={mergeStyles(\n        mergeStyles(\n          recipeStyle(cn(MESSAGE_SCROLLER_CONTENT, className), theme),\n          {\n            gap: theme.spacing.lg,\n          },\n        ),\n        style,\n      )}\n    >\n      {children}\n    </div>\n  );\n};\n\nexport interface MessageScrollerItemProps extends Omit<Props, \"children\"> {\n  readonly scrollAnchor?: boolean;\n\n  readonly className?: string;\n  readonly children?: ReactNode;\n  readonly ref?: Ref<PublicInstance>;\n}\n\nexport const MessageScrollerItem = ({\n  scrollAnchor = false,\n  children,\n  className,\n  style,\n  ref,\n  onLayout,\n  ...rest\n}: MessageScrollerItemProps) => {\n  const theme = useTheme();\n  const scroller = useMessageScroller();\n  const itemRef = useRef<PublicInstance | null>(null);\n  const mergedRef = useMergedRef(itemRef, ref);\n  const first = useRef(true);\n\n  useEffect(() => {\n    if (scroller.ready.current) scroller.arrived();\n  }, [scroller.arrived, scroller.ready]);\n\n  return (\n    <div\n      {...rest}\n      ref={mergedRef}\n      onLayout={(event) => {\n        if (\n          !first.current &&\n          scroller.pinned.current &&\n          scroller.ready.current\n        ) {\n          scroller.scrollToEnd.current();\n        }\n        if (first.current) {\n          first.current = false;\n          if (\n            scrollAnchor &&\n            scroller.ready.current &&\n            scroller.pinned.current\n          ) {\n            const instance = itemRef.current;\n            if (instance !== null) scroller.alignTo.current(instance);\n          }\n        }\n        onLayout?.(event);\n      }}\n      style={mergeStyles(\n        recipeStyle(cn(MESSAGE_SCROLLER_ITEM, className), theme),\n        style,\n      )}\n    >\n      {children}\n    </div>\n  );\n};\n\nexport interface MessageScrollerJumpToLatestProps extends Omit<\n  Props,\n  \"children\"\n> {\n  readonly label?: string;\n\n  readonly className?: string;\n}\n\nexport const MessageScrollerJumpToLatest = ({\n  label = \"Jump to latest\",\n  className,\n  style,\n  onClick,\n  ...rest\n}: MessageScrollerJumpToLatestProps) => {\n  const theme = useTheme();\n  const scroller = useMessageScroller();\n  const state = useMessageScrollerState();\n  const active = !state.pinned;\n  const text =\n    state.unread === 0\n      ? label\n      : `${state.unread} new ${state.unread === 1 ? \"message\" : \"messages\"}`;\n\n  if (!active) return null;\n\n  return (\n    <div\n      style={mergeStyles(recipeStyle(MESSAGE_SCROLLER_JUMP, theme), {\n        position: \"absolute\",\n        left: 0,\n        right: 0,\n        bottom: theme.spacing.lg,\n      })}\n    >\n      <div\n        motion={{\n          initial: { opacity: 0, top: 4 },\n          animate: { opacity: 1, top: 0 },\n          transition: { duration: 0.2, ease: \"easeOut\" },\n        }}\n        style={{ pointerEvents: \"auto\" }}\n      >\n        <Button\n          {...rest}\n          variant=\"secondary\"\n          size=\"sm\"\n          onClick={(event) => {\n            scroller.jump();\n            onClick?.(event);\n          }}\n          style={mergeStyles(recipeStyle(cn(className ?? \"\"), theme), style)}\n        >\n          {text}\n        </Button>\n      </div>\n    </div>\n  );\n};\n\nconst messageScrollerFamily: ReadonlyArray<PartComponent> = [\n  MessageScroller,\n  MessageScrollerViewport,\n  MessageScrollerContent,\n  MessageScrollerItem,\n  MessageScrollerJumpToLatest,\n];\n\nconst messageScrollerRoot: PartsContract = {\n  parent: \"MessageScroller\",\n  family: messageScrollerFamily,\n  accepts: [MessageScrollerViewport, MessageScrollerJumpToLatest],\n  hint: \"Put MessageScrollerViewport and MessageScrollerJumpToLatest directly inside it.\",\n};\n\nconst messageScrollerViewport: PartsContract = {\n  parent: \"MessageScrollerViewport\",\n  family: messageScrollerFamily,\n  accepts: [MessageScrollerContent, MessageScrollerItem],\n  hint: \"Put MessageScrollerContent (or items directly) inside it.\",\n};\n\nconst messageScrollerContent: PartsContract = {\n  parent: \"MessageScrollerContent\",\n  family: messageScrollerFamily,\n  accepts: [MessageScrollerItem],\n  hint: \"Put MessageScrollerItem directly inside it.\",\n};\n\nexport const messageScrollerMeta = {\n  name: \"Message scroller\",\n  tier: 2,\n  status: \"done\",\n  gpuixSupport: \"partial\",\n  limitations: [\n    \"the viewport renders every row: <virtual-list> takes itemCount rather than compound children, so long histories are not virtualized\",\n    \"a row that grows in place is followed through its own onLayout size report while pinned; there is no resize observer for a row outside the item wrapper, which is intentional\",\n    \"no aria-live role in 0.11.0, so new rows are not announced; the jump control's count is painted\",\n    \"a threshold or arrival count it cannot read (NaN) is ignored, so the pin state is kept and the affordance never paints NaN\",\n  ],\n  docs: \"docs/message-scroller.mdx\",\n  example: \"examples/message-scroller.example.tsx\",\n  testFile: \"src/behaviour/message-scroller.test.tsx\",\n} as const;\n\nexport type { MessageScrollerState };\n"
    },
    {
      "path": "packages/components/src/message-scroller/store.ts",
      "target": "@ui/gpuixui/components/message-scroller/store.ts",
      "type": "registry:lib",
      "content": "import { Effect } from \"effect\";\nimport * as SubscriptionRef from \"effect/SubscriptionRef\";\nimport { makeLocalState, type LocalState } from \"../internal/local-state.js\";\n\nexport interface MessageScrollerState {\n  readonly pinned: boolean;\n\n  readonly unread: number;\n\n  readonly streaming: boolean;\n}\n\nexport const initialMessageScrollerState = (\n  pinned: boolean,\n): MessageScrollerState => ({ pinned, unread: 0, streaming: false });\n\nexport interface ScrollObservation {\n  readonly distanceFromEnd: number;\n\n  readonly threshold: number;\n}\n\nexport const observeScroll = (\n  state: MessageScrollerState,\n  { distanceFromEnd, threshold }: ScrollObservation,\n): MessageScrollerState => {\n  if (Number.isNaN(distanceFromEnd) || Number.isNaN(threshold)) return state;\n  const pinned = distanceFromEnd <= Math.max(0, threshold);\n\n  const unread = pinned ? 0 : state.unread;\n  return pinned === state.pinned && unread === state.unread\n    ? state\n    : { ...state, pinned, unread };\n};\n\nexport const addItems = (\n  state: MessageScrollerState,\n  count: number,\n): MessageScrollerState =>\n  state.pinned || Number.isNaN(count) || count <= 0\n    ? state\n    : { ...state, unread: state.unread + count };\n\nexport const requestLatest = (\n  state: MessageScrollerState,\n): MessageScrollerState =>\n  state.pinned && state.unread === 0\n    ? state\n    : { ...state, pinned: true, unread: 0 };\n\nexport interface MessageScrollerStore {\n  readonly local: LocalState<MessageScrollerState>;\n\n  readonly observe: (observation: ScrollObservation) => Effect.Effect<boolean>;\n\n  readonly arrived: Effect.Effect<void>;\n\n  readonly jump: Effect.Effect<void>;\n\n  readonly stream: (streaming: boolean) => Effect.Effect<void>;\n\n  readonly setPinned: (pinned: boolean) => Effect.Effect<void>;\n}\n\nexport const makeMessageScrollerStore = (\n  defaultPinned: boolean,\n): Effect.Effect<MessageScrollerStore> =>\n  Effect.gen(function* () {\n    const local = yield* makeLocalState(\n      initialMessageScrollerState(defaultPinned),\n    );\n\n    const observe = Effect.fnUntraced(function* (\n      observation: ScrollObservation,\n    ) {\n      const current = yield* SubscriptionRef.get(local.state);\n      const next = observeScroll(current, observation);\n      if (next !== current) yield* local.write(next);\n      return next.pinned;\n    });\n\n    const arrived = Effect.gen(function* () {\n      const current = yield* SubscriptionRef.get(local.state);\n      const next = addItems(current, 1);\n      if (next !== current) yield* local.write(next);\n    });\n\n    const jump = Effect.gen(function* () {\n      const current = yield* SubscriptionRef.get(local.state);\n      const next = requestLatest(current);\n      if (next !== current) yield* local.write(next);\n    });\n\n    const stream = Effect.fnUntraced(function* (streaming: boolean) {\n      const current = yield* SubscriptionRef.get(local.state);\n      if (current.streaming === streaming) return;\n      yield* local.write({ ...current, streaming });\n    });\n\n    const setPinned = Effect.fnUntraced(function* (pinned: boolean) {\n      const current = yield* SubscriptionRef.get(local.state);\n      if (current.pinned === pinned) return;\n      yield* local.write({\n        ...current,\n        pinned,\n        unread: pinned ? 0 : current.unread,\n      });\n    });\n\n    return { local, observe, arrived, jump, stream, setPinned };\n  });\n"
    }
  ],
  "docs": "[Chat](https://gpuixui.sudokw.com/components/chat) — These are GPUix native components: the files render through GPUix's reconciler against `@gpuix/react`, not the DOM, and need the per-platform `@gpuix/native` binding. The CLI installs the `@gpuixui/*` runtime the components import as registry items, so the copied tree resolves on its own; wrap it in `ThemeProvider` before it renders.",
  "meta": {
    "url": "/components/chat",
    "kind": "component",
    "runtime": "gpuix",
    "registry": "gpuixui"
  }
}