{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "connect-button",
  "title": "Connect Button",
  "description": "One button for the whole session lifecycle, driven by the Pipecat transport state. Every state's label, icon, style, and action can be overridden sparsely.",
  "dependencies": [
    "@pipecat-ai/client-js@^1.13.1",
    "@pipecat-ai/client-react@^1.8.2",
    "lucide-react@^1.41.0"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "src/components/connect-button.tsx",
      "content": "\"use client\";\n\nimport type { TransportState } from \"@pipecat-ai/client-js\";\nimport {\n  usePipecatClient,\n  usePipecatClientTransportState,\n} from \"@pipecat-ai/client-react\";\nimport {\n  Loader2Icon,\n  PhoneIcon,\n  PhoneOffIcon,\n  RefreshCwIcon,\n} from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\ntype ButtonProps = React.ComponentProps<typeof Button>;\n\n/**\n * Appearance and behavior of the button in a single transport state.\n * Everything is optional — an unset field falls back to the matching\n * top-level prop, then to the built-in default for that state.\n */\nexport interface ConnectButtonStateProps {\n  /** Label for this state. Pass null to render no label (add an aria-label). */\n  children?: React.ReactNode;\n  /** Leading icon. Transitional states default to a spinner; null removes it. */\n  icon?: React.ReactNode;\n  /**\n   * Button variant for this state. Setting a variant — here or at the top\n   * level — replaces the default token styling for the state.\n   */\n  variant?: ButtonProps[\"variant\"];\n  /** Extra classes for this state, merged after the top-level className. */\n  className?: string;\n  /** Whether the button is disabled. Transitional states default to true. */\n  disabled?: boolean;\n  /** Replaces the default connect/disconnect action for this state. */\n  onClick?: React.MouseEventHandler<HTMLButtonElement>;\n}\n\n/**\n * Sparse per-state overrides, keyed by Pipecat's TransportState. Specify\n * only the states you want to change; everything else keeps its default.\n */\nexport type ConnectButtonStateMap = Partial<\n  Record<TransportState, ConnectButtonStateProps>\n>;\n\nconst SPINNER = <Loader2Icon className=\"animate-spin\" />;\n\n// Nova's icon-* sizes render a square button; the view responds by going\n// icon-only (no label, no min-width).\nfunction isIconSize(size: ButtonProps[\"size\"]): boolean {\n  return typeof size === \"string\" && size.startsWith(\"icon\");\n}\n\n// Default glyphs for icon-only rendering. Text sizes stay label-first\n// (spinner only on transitional states), so these apply only when an\n// icon-* size is used and neither the state nor the consumer set an icon.\nconst DEFAULT_STATE_ICONS: Record<TransportState, React.ReactNode> = {\n  disconnected: <PhoneIcon />,\n  initializing: SPINNER,\n  initialized: <PhoneIcon />,\n  authenticating: SPINNER,\n  authenticated: SPINNER,\n  connecting: SPINNER,\n  connected: <PhoneOffIcon />,\n  ready: <PhoneOffIcon />,\n  disconnecting: SPINNER,\n  error: <RefreshCwIcon />,\n};\n\n// Idle states: solid active-token fill — the kit's \"go\" affordance.\nconst CONNECT_CLASSES =\n  \"bg-active text-active-foreground hover:bg-active/85 focus-visible:ring-active/40 focus-visible:border-active\";\n// Connected states: tinted inactive-token surface — the kit's \"stop\"\n// affordance (nova's destructive-button idiom, as on the screen control).\nconst DISCONNECT_CLASSES =\n  \"border-inactive/30 bg-inactive/10 text-inactive hover:bg-inactive/20 hover:text-inactive dark:bg-inactive/20\";\n\nconst DEFAULT_STATE_PROPS: Record<TransportState, ConnectButtonStateProps> = {\n  disconnected: { children: \"Connect\", className: CONNECT_CLASSES },\n  initializing: {\n    children: \"Initializing…\",\n    icon: SPINNER,\n    variant: \"secondary\",\n    disabled: true,\n  },\n  initialized: { children: \"Connect\", className: CONNECT_CLASSES },\n  authenticating: {\n    children: \"Connecting…\",\n    icon: SPINNER,\n    variant: \"secondary\",\n    disabled: true,\n  },\n  authenticated: {\n    children: \"Connecting…\",\n    icon: SPINNER,\n    variant: \"secondary\",\n    disabled: true,\n  },\n  connecting: {\n    children: \"Connecting…\",\n    icon: SPINNER,\n    variant: \"secondary\",\n    disabled: true,\n  },\n  connected: {\n    children: \"Disconnect\",\n    variant: \"outline\",\n    className: DISCONNECT_CLASSES,\n  },\n  ready: {\n    children: \"Disconnect\",\n    variant: \"outline\",\n    className: DISCONNECT_CLASSES,\n  },\n  disconnecting: {\n    children: \"Disconnecting…\",\n    icon: SPINNER,\n    variant: \"secondary\",\n    disabled: true,\n  },\n  error: { children: \"Retry\", variant: \"destructive\" },\n};\n\n/** States where the transport is between stable endpoints. */\nconst TRANSITIONAL_STATES: TransportState[] = [\n  \"initializing\",\n  \"authenticating\",\n  \"authenticated\",\n  \"connecting\",\n  \"disconnecting\",\n];\n\n/** States where clicking tears the session down rather than starting one. */\nconst DISCONNECT_STATES: TransportState[] = [\n  \"authenticating\",\n  \"authenticated\",\n  \"connecting\",\n  \"connected\",\n  \"ready\",\n];\n\nexport interface ConnectButtonViewProps extends Omit<\n  ButtonProps,\n  \"children\" | \"onClick\"\n> {\n  /** Transport state driving the button. */\n  transportState?: TransportState;\n  /** Called when clicked in a state that starts a session (disconnected, initialized, error). */\n  onConnect?: () => void;\n  /** Called when clicked in a state that ends one (connecting through ready). */\n  onDisconnect?: () => void;\n  /** Called on every click, before the state action. */\n  onClick?: React.MouseEventHandler<HTMLButtonElement>;\n  /** Label used for every state, unless a state overrides it. */\n  children?: React.ReactNode;\n  /** Sparse per-state overrides for label, icon, style, and action. */\n  stateProps?: ConnectButtonStateMap;\n}\n\n/** State overrides take precedence over component props, then defaults. Icon-only buttons use the label as their accessible name. */\nexport function ConnectButtonView({\n  transportState = \"disconnected\",\n  onConnect,\n  onDisconnect,\n  onClick,\n  children,\n  stateProps,\n  variant,\n  size,\n  className,\n  disabled,\n  ...props\n}: ConnectButtonViewProps) {\n  const defaults = DEFAULT_STATE_PROPS[transportState];\n  const overrides = stateProps?.[transportState] ?? {};\n  const iconOnly = isIconSize(size);\n\n  // null is meaningful for content (renders nothing); undefined falls back.\n  const resolvedLabel =\n    overrides.children !== undefined\n      ? overrides.children\n      : children !== undefined\n        ? children\n        : defaults.children;\n  const label = iconOnly ? null : resolvedLabel;\n  const icon =\n    overrides.icon !== undefined\n      ? overrides.icon\n      : iconOnly\n        ? (defaults.icon ?? DEFAULT_STATE_ICONS[transportState])\n        : defaults.icon;\n  // An explicit variant opts out of the default token styling, which is\n  // designed around the state's default variant.\n  const stateClassName =\n    (overrides.variant ?? variant) ? undefined : defaults.className;\n\n  const handleClick: React.MouseEventHandler<HTMLButtonElement> = (event) => {\n    onClick?.(event);\n    if (overrides.onClick) {\n      overrides.onClick(event);\n    } else if (DISCONNECT_STATES.includes(transportState)) {\n      onDisconnect?.();\n    } else {\n      onConnect?.();\n    }\n  };\n\n  return (\n    <Button\n      data-slot=\"connect-button\"\n      data-state={transportState}\n      variant={overrides.variant ?? variant ?? defaults.variant}\n      size={size}\n      disabled={disabled || (overrides.disabled ?? defaults.disabled)}\n      aria-busy={TRANSITIONAL_STATES.includes(transportState) || undefined}\n      aria-label={\n        iconOnly && typeof resolvedLabel === \"string\"\n          ? resolvedLabel\n          : undefined\n      }\n      onClick={handleClick}\n      className={cn(\n        !iconOnly && \"min-w-32\",\n        stateClassName,\n        className,\n        overrides.className,\n      )}\n      {...props}\n    >\n      {icon}\n      {label}\n    </Button>\n  );\n}\n\nexport type ConnectButtonProps = Omit<ConnectButtonViewProps, \"transportState\">;\n\n/** Requires PipecatClientProvider. Pass onConnect when your app owns bot startup. */\nexport function ConnectButton({\n  onConnect,\n  onDisconnect,\n  ...props\n}: ConnectButtonProps) {\n  const client = usePipecatClient();\n  const transportState = usePipecatClientTransportState();\n\n  const handleConnect = () => {\n    if (onConnect) {\n      onConnect();\n      return;\n    }\n    try {\n      // Connection failures surface through the transport's \"error\" state.\n      client?.connect().catch(() => {});\n    } catch {\n      // connect() can also throw synchronously (e.g. missing connection\n      // params); the client reports it, nothing more to do here.\n    }\n  };\n\n  const handleDisconnect = () => {\n    if (onDisconnect) {\n      onDisconnect();\n      return;\n    }\n    client?.disconnect().catch(() => {});\n  };\n\n  return (\n    <ConnectButtonView\n      transportState={transportState}\n      onConnect={handleConnect}\n      onDisconnect={handleDisconnect}\n      {...props}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/pipecat/connect-button.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-active": "var(--active-background)",
      "color-active-foreground": "var(--active-foreground)",
      "color-inactive": "var(--inactive-background)",
      "color-inactive-foreground": "var(--inactive-foreground)"
    },
    "light": {
      "active-background": "oklch(0.696 0.17 162.48)",
      "active-foreground": "oklch(0.979 0.021 166.113)",
      "inactive-background": "oklch(0.702 0.189 22.23)",
      "inactive-foreground": "oklch(0.971 0.013 17.38)"
    },
    "dark": {
      "active-background": "oklch(0.702 0.158 160.44)",
      "active-foreground": "oklch(0.979 0.021 166.11)",
      "inactive-background": "oklch(0.702 0.189 22.23)",
      "inactive-foreground": "oklch(0.971 0.013 17.38)"
    }
  },
  "docs": "Render inside a PipecatClientProvider. Without onConnect/onDisconnect it calls client.connect()/client.disconnect(); use stateProps to override individual transport states.",
  "categories": [
    "voice"
  ],
  "type": "registry:component"
}