{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "provider-sign-in-button", "title": "Provider Sign-In Button", "author": "MR ", "description": "A sign-in button for a single OAuth provider: brand icon, label, loading state. Works as a link or a click handler.", "dependencies": [ "lucide-react" ], "registryDependencies": [ "button", "@cantera/oauth-types" ], "files": [ { "path": "registry/ui/provider-sign-in-button.tsx", "content": "'use client'\n\nimport { LoaderCircleIcon } from 'lucide-react'\nimport type * as React from 'react'\nimport { useState } from 'react'\n\nimport { Button } from '@/components/ui/button'\nimport type { OAuthProvider } from '@/lib/oauth-types'\nimport { cn } from '@/lib/utils'\n\ninterface ProviderSignInButtonBaseProps {\n provider: OAuthProvider\n /**\n * Pending: the control stays mounted, keeps its label, and crossfades its\n * icon to a spinner. Drive it from a server action, or let the component\n * derive it from an `onSignIn` that returns a promise.\n */\n loading?: boolean\n /**\n * Rendered as `aria-disabled`, never the native attribute, so the control\n * keeps focus and stays discoverable to a screen reader.\n */\n disabled?: boolean\n variant?: 'default' | 'outline' | 'secondary' | 'ghost'\n size?: 'default' | 'sm' | 'lg'\n}\n\n/**\n * One element per mode, in every state: with `href` this is always an anchor —\n * loading included — and without it always a button. Props are forwarded to\n * whichever element renders, and typed for it.\n */\ntype ProviderSignInButtonProps = ProviderSignInButtonBaseProps &\n (\n | ({\n /** Navigate to an auth route instead of handling a click. Renders an anchor. */\n href: string\n onSignIn?: never\n } & Omit, 'href'>)\n | ({\n href?: never\n /** Called with no arguments when the button is clicked. */\n onSignIn?: () => void | Promise\n } & Omit, 'disabled'>)\n )\n\n/**\n * The icon slot is always reserved — same box whether the provider has a mark\n * or not — so starting a sign-in never shifts the label. Icon and spinner\n * stack in one grid cell and crossfade over 150ms.\n */\nfunction ProviderSignInIcon({ provider, loading }: { provider: OAuthProvider; loading: boolean }) {\n return (\n \n \n {provider.icon && (\n \n {provider.icon}\n \n )}\n \n )\n}\n\n/**\n * A sign-in button for a single OAuth provider: brand icon, label, and a\n * pending state. Works for any provider — pass an OAuthProvider shape.\n */\nfunction ProviderSignInButton(props: ProviderSignInButtonProps) {\n const [asyncPending, setAsyncPending] = useState(false)\n const {\n provider,\n loading = false,\n disabled = false,\n variant = 'outline',\n size = 'lg',\n className,\n children,\n } = props\n\n const pending = loading || asyncPending\n const inert = pending || disabled\n const label = children ?? `Continue with ${provider.name}`\n const classes = cn(\n 'w-full justify-center gap-2',\n // 44px minimum touch target: this is a primary action, used with gloves on\n // a tablet. `size=\"sm\"` is the opt-in compact escape hatch.\n size !== 'sm' && 'min-h-11',\n 'aria-disabled:pointer-events-none',\n disabled && !pending && 'opacity-50',\n className,\n )\n const icon = \n\n if (props.href !== undefined) {\n const {\n provider: _provider,\n loading: _loading,\n disabled: _disabled,\n variant: _variant,\n size: _size,\n className: _className,\n children: _children,\n onSignIn: _onSignIn,\n href,\n ...anchorProps\n } = props\n\n return (\n }\n nativeButton={false}\n // The primitive assumes button semantics for a non-native element;\n // this is a navigation link, so keep the link role it earns from href.\n role=\"link\"\n disabled={inert}\n focusableWhenDisabled\n data-slot=\"provider-sign-in-button\"\n aria-busy={pending || undefined}\n variant={variant}\n size={size}\n className={classes}\n >\n {icon}\n {label}\n \n )\n }\n\n const {\n provider: _provider,\n loading: _loading,\n disabled: _disabled,\n variant: _variant,\n size: _size,\n className: _className,\n children: _children,\n href: _href,\n onSignIn,\n ...buttonProps\n } = props\n\n return (\n {\n buttonProps.onClick?.(event)\n const result = onSignIn?.()\n if (!(result instanceof Promise)) return\n setAsyncPending(true)\n result.then(\n () => setAsyncPending(false),\n () => setAsyncPending(false),\n )\n }}\n >\n {icon}\n {label}\n \n )\n}\n\nexport { ProviderSignInButton, type ProviderSignInButtonProps }\n", "type": "registry:ui" } ], "categories": [ "authentication", "buttons" ], "type": "registry:component" }