{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "profile-card",
  "title": "Profile Card",
  "author": "Satchmo",
  "description": "BAP identity profile card with avatar, name, bio, identity key, and follow action slot. Fetches profile data from the 1sat-stack BAP API.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "card",
    "badge",
    "separator",
    "skeleton",
    "button",
    "https://registry.bigblocks.dev/r/bitcoin-avatar.json"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/profile-card/index.tsx",
      "content": "\"use client\"\n\nimport { ProfileCardUI, type ProfileCardUIProps } from \"./profile-card-ui\"\nimport {\n  useProfileCard,\n  type UseProfileCardOptions,\n  type UseProfileCardReturn,\n  type BapProfile,\n} from \"./use-profile-card\"\nimport type { ReactNode } from \"react\"\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport { ProfileCardUI, type ProfileCardUIProps } from \"./profile-card-ui\"\nexport {\n  useProfileCard,\n  type UseProfileCardOptions,\n  type UseProfileCardReturn,\n  type BapProfile,\n} from \"./use-profile-card\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ProfileCardProps {\n  /** BAP ID to look up directly */\n  bapId?: string\n  /** Bitcoin address to resolve to a BAP identity */\n  address?: string\n  /** Base URL for the 1sat-stack API (default: https://api.1sat.app) */\n  apiUrl?: string\n  /** Optional CSS class name */\n  className?: string\n  /** Render prop for a follow button or other action in the header */\n  renderAction?: (bapId: string) => ReactNode\n  /** Callback to handle external links (e.g. open in system browser from a WebView) */\n  onExternalLink?: (url: string) => void\n}\n\n// ---------------------------------------------------------------------------\n// Composed component\n// ---------------------------------------------------------------------------\n\n/**\n * Display a BAP identity profile card with avatar, name, bio,\n * identity key, and an optional action slot (e.g. follow button).\n *\n * Fetches profile data from the 1sat-stack BAP API. Provide either\n * a `bapId` for direct lookup or an `address` to resolve the identity.\n *\n * @example\n * ```tsx\n * import { ProfileCard } from \"@/components/blocks/profile-card\"\n *\n * <ProfileCard bapId=\"Go8vCHAa4S6AhXKdRp3nT9wJm\" />\n *\n * <ProfileCard\n *   address=\"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa\"\n *   renderAction={(bapId) => <FollowButton bapId={bapId} />}\n * />\n * ```\n */\nexport function ProfileCard({\n  bapId: bapIdProp,\n  address,\n  apiUrl,\n  className,\n  renderAction,\n  onExternalLink,\n}: ProfileCardProps) {\n  const hook = useProfileCard({ bapId: bapIdProp, address, apiUrl })\n\n  return (\n    <ProfileCardUI\n      className={className}\n      bapId={hook.bapId}\n      profile={hook.profile}\n      currentAddress={hook.currentAddress}\n      isLoading={hook.isLoading}\n      error={hook.error}\n      onRetry={hook.refetch}\n      renderAction={renderAction}\n      onExternalLink={onExternalLink}\n    />\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/profile-card/index.tsx"
    },
    {
      "path": "registry/new-york/blocks/profile-card/profile-card-ui.tsx",
      "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  Card,\n  CardContent,\n  CardHeader,\n} from \"@/components/ui/card\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { BitcoinAvatar } from \"@/registry/new-york/blocks/bitcoin-avatar\"\nimport { AlertCircle, ExternalLink, RefreshCw } from \"lucide-react\"\nimport { Button } from \"@/components/ui/button\"\nimport type { BapProfile } from \"./use-profile-card\"\nimport type { ReactNode } from \"react\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ProfileCardUIProps {\n  /** Optional CSS class name */\n  className?: string\n  /** BAP ID (~27 char base58 string) */\n  bapId: string | null\n  /** Parsed profile data */\n  profile: BapProfile | null\n  /** Current signing address for avatar resolution */\n  currentAddress: string | null\n  /** Whether the profile is loading */\n  isLoading: boolean\n  /** Error from fetching */\n  error: Error | null\n  /** Retry callback for error state */\n  onRetry?: () => void\n  /** Render prop for a follow button or other action in the header */\n  renderAction?: (bapId: string) => ReactNode\n  /** Callback to handle external links (e.g. open in system browser from a WebView) */\n  onExternalLink?: (url: string) => void\n}\n\n// ---------------------------------------------------------------------------\n// Loading skeleton\n// ---------------------------------------------------------------------------\n\nfunction ProfileCardSkeleton({ className }: { className?: string }) {\n  return (\n    <Card className={cn(\"w-full max-w-sm\", className)}>\n      <CardHeader className=\"flex flex-row items-start gap-4 pb-3\">\n        <Skeleton className=\"size-16 shrink-0 rounded-full\" />\n        <div className=\"flex-1 flex flex-col gap-2 pt-1\">\n          <Skeleton className=\"h-5 w-32\" />\n          <Skeleton className=\"h-4 w-24\" />\n        </div>\n      </CardHeader>\n      <CardContent className=\"flex flex-col gap-3\">\n        <Skeleton className=\"h-4 w-full\" />\n        <Skeleton className=\"h-4 w-3/4\" />\n        <Separator />\n        <Skeleton className=\"h-4 w-48\" />\n      </CardContent>\n    </Card>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// Error state\n// ---------------------------------------------------------------------------\n\nfunction ProfileCardError({\n  className,\n  error,\n  onRetry,\n}: {\n  className?: string\n  error: Error\n  onRetry?: () => void\n}) {\n  return (\n    <Card className={cn(\"w-full max-w-sm\", className)}>\n      <CardContent className=\"flex flex-col items-center gap-3 py-8\">\n        <div className=\"flex size-10 items-center justify-center rounded-full bg-destructive/10\">\n          <AlertCircle className=\"size-5 text-destructive\" />\n        </div>\n        <p className=\"text-sm text-muted-foreground text-center\">\n          {error.message}\n        </p>\n        {onRetry && (\n          <Button variant=\"outline\" size=\"sm\" onClick={onRetry}>\n            <RefreshCw data-icon=\"inline-start\" />\n            Retry\n          </Button>\n        )}\n      </CardContent>\n    </Card>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// Main UI component\n// ---------------------------------------------------------------------------\n\nexport function ProfileCardUI({\n  className,\n  bapId,\n  profile,\n  currentAddress,\n  isLoading,\n  error,\n  onRetry,\n  renderAction,\n  onExternalLink,\n}: ProfileCardUIProps) {\n  if (isLoading) {\n    return <ProfileCardSkeleton className={className} />\n  }\n\n  if (error) {\n    return (\n      <ProfileCardError\n        className={className}\n        error={error}\n        onRetry={onRetry}\n      />\n    )\n  }\n\n  if (!bapId) {\n    return (\n      <Card className={cn(\"w-full max-w-sm\", className)}>\n        <CardContent className=\"flex flex-col items-center gap-2 py-8\">\n          <p className=\"text-sm text-muted-foreground\">\n            No identity found\n          </p>\n        </CardContent>\n      </Card>\n    )\n  }\n\n  const displayName =\n    profile?.name ?? profile?.alternateName ?? bapId.slice(0, 8)\n  const handle = profile?.alternateName\n    ? profile.alternateName.startsWith(\"@\")\n      ? profile.alternateName\n      : `@${profile.alternateName}`\n    : null\n  const avatarAddress = currentAddress ?? bapId\n\n  return (\n    <Card className={cn(\"w-full max-w-sm\", className)}>\n      <CardHeader className=\"flex flex-row items-start gap-4 pb-3\">\n        <BitcoinAvatar\n          address={avatarAddress}\n          imageUrl={profile?.image}\n          size=\"lg\"\n          className=\"shrink-0\"\n        />\n        <div className=\"flex-1 min-w-0 flex flex-col gap-1 pt-0.5\">\n          <div className=\"flex items-start justify-between gap-2\">\n            <div className=\"min-w-0\">\n              <p className=\"text-base font-semibold leading-tight truncate\">\n                {displayName}\n              </p>\n              {handle && (\n                <p className=\"text-sm text-muted-foreground truncate\">\n                  {handle}\n                </p>\n              )}\n            </div>\n            {renderAction && bapId ? (\n              <div className=\"shrink-0\">{renderAction(bapId)}</div>\n            ) : null}\n          </div>\n        </div>\n      </CardHeader>\n\n      <CardContent className=\"flex flex-col gap-3 pt-0\">\n        {profile?.description && (\n          <p className=\"text-sm text-muted-foreground leading-relaxed\">\n            {profile.description}\n          </p>\n        )}\n\n        <Separator />\n\n        <div className=\"flex flex-col gap-1.5\">\n          <p className=\"text-xs font-medium text-muted-foreground\">BAP ID</p>\n          <Badge\n            variant=\"secondary\"\n            className=\"font-mono text-xs break-all whitespace-normal\"\n          >\n            {bapId}\n          </Badge>\n        </div>\n\n        {profile?.url && (\n          onExternalLink ? (\n            <button\n              type=\"button\"\n              onClick={() => onExternalLink(profile.url as string)}\n              className=\"inline-flex items-center gap-1 text-xs text-primary hover:underline transition-colors\"\n            >\n              {profile.url}\n              <ExternalLink className=\"size-3 flex-shrink-0\" />\n            </button>\n          ) : (\n            <a\n              href={profile.url}\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"inline-flex items-center gap-1 text-xs text-primary hover:underline transition-colors\"\n            >\n              {profile.url}\n              <ExternalLink className=\"size-3 flex-shrink-0\" />\n            </a>\n          )\n        )}\n      </CardContent>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/profile-card/profile-card-ui.tsx"
    },
    {
      "path": "registry/new-york/blocks/profile-card/use-profile-card.ts",
      "content": "\"use client\"\n\nimport { useCallback, useEffect, useState } from \"react\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Schema.org profile fields returned by the 1sat-stack BAP API */\nexport interface BapProfile {\n  /** Display name (givenName + familyName or name) */\n  name?: string\n  /** Handle / alternate name (e.g. @satoshi) */\n  alternateName?: string\n  /** Given (first) name */\n  givenName?: string\n  /** Family (last) name */\n  familyName?: string\n  /** Short bio / description */\n  description?: string\n  /** Avatar image URL (may be ord://, b://, or https://) */\n  image?: string\n  /** Home page or website URL */\n  url?: string\n}\n\n/** BAP identity record from /1sat/bap/identity/get */\ninterface BapIdentityRecord {\n  idKey: string\n  firstSeen: number\n  rootAddress: string\n  currentAddress: string\n  addresses: Array<{ address: string; txId: string; block: number }>\n  identity?: Record<string, unknown>\n}\n\n/** Response from POST /1sat/bap/identity/validByAddress */\ninterface BapValidByAddressResponse {\n  identity: BapIdentityRecord\n  validityRecord: { valid: boolean; block: number }\n  profile?: Record<string, unknown>\n}\n\nexport interface UseProfileCardOptions {\n  /** BAP ID to look up directly */\n  bapId?: string\n  /** Bitcoin address to resolve to a BAP identity */\n  address?: string\n  /** Base URL for the 1sat-stack API (default: https://api.1sat.app) */\n  apiUrl?: string\n}\n\nexport interface UseProfileCardReturn {\n  /** Resolved BAP ID */\n  bapId: string | null\n  /** Parsed profile data */\n  profile: BapProfile | null\n  /** Current signing address */\n  currentAddress: string | null\n  /** Whether profile data is loading */\n  isLoading: boolean\n  /** Error if fetch failed */\n  error: Error | null\n  /** Refetch profile data */\n  refetch: () => void\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_API_URL = \"https://api.1sat.app\"\n\nfunction parseProfile(raw: Record<string, unknown>): BapProfile {\n  return {\n    name: typeof raw.name === \"string\" ? raw.name : undefined,\n    alternateName:\n      typeof raw.alternateName === \"string\" ? raw.alternateName : undefined,\n    givenName: typeof raw.givenName === \"string\" ? raw.givenName : undefined,\n    familyName: typeof raw.familyName === \"string\" ? raw.familyName : undefined,\n    description:\n      typeof raw.description === \"string\" ? raw.description : undefined,\n    image: typeof raw.image === \"string\" ? raw.image : undefined,\n    url: typeof raw.url === \"string\" ? raw.url : undefined,\n  }\n}\n\nfunction buildDisplayName(profile: BapProfile): string | undefined {\n  if (profile.name) return profile.name\n  if (profile.givenName || profile.familyName) {\n    return [profile.givenName, profile.familyName].filter(Boolean).join(\" \")\n  }\n  return undefined\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\nexport function useProfileCard({\n  bapId: bapIdProp,\n  address,\n  apiUrl = DEFAULT_API_URL,\n}: UseProfileCardOptions): UseProfileCardReturn {\n  const [bapId, setBapId] = useState<string | null>(bapIdProp ?? null)\n  const [profile, setProfile] = useState<BapProfile | null>(null)\n  const [currentAddress, setCurrentAddress] = useState<string | null>(null)\n  const [isLoading, setIsLoading] = useState(false)\n  const [error, setError] = useState<Error | null>(null)\n  const [fetchKey, setFetchKey] = useState(0)\n\n  const refetch = useCallback(() => {\n    setFetchKey((k) => k + 1)\n  }, [])\n\n  useEffect(() => {\n    if (!bapIdProp && !address) return\n\n    let cancelled = false\n    setIsLoading(true)\n    setError(null)\n\n    async function fetchProfile() {\n      try {\n        let resolvedBapId = bapIdProp ?? null\n        let identityData: BapIdentityRecord | null = null\n\n        // If we have an address but no bapId, resolve via validByAddress\n        if (!resolvedBapId && address) {\n          const res = await fetch(\n            `${apiUrl}/1sat/bap/identity/validByAddress`,\n            {\n              method: \"POST\",\n              headers: { \"Content-Type\": \"application/json\" },\n              body: JSON.stringify({ address, block: 0 }),\n            },\n          )\n          if (!res.ok) {\n            throw new Error(`Failed to resolve address: ${res.status}`)\n          }\n          const data: BapValidByAddressResponse = await res.json()\n          resolvedBapId = data.identity.idKey\n          identityData = data.identity\n        }\n\n        if (cancelled) return\n\n        // Fetch the identity by bapId if we don't have it yet\n        if (resolvedBapId && !identityData) {\n          const res = await fetch(`${apiUrl}/1sat/bap/identity/get`, {\n            method: \"POST\",\n            headers: { \"Content-Type\": \"application/json\" },\n            body: JSON.stringify({ idKey: resolvedBapId }),\n          })\n          if (!res.ok) {\n            throw new Error(`Failed to fetch identity: ${res.status}`)\n          }\n          const fetched: BapIdentityRecord = await res.json()\n          identityData = fetched\n        }\n\n        if (cancelled) return\n\n        // Fetch the profile data\n        if (resolvedBapId) {\n          const profileRes = await fetch(\n            `${apiUrl}/1sat/bap/profile/${encodeURIComponent(resolvedBapId)}`,\n          )\n          if (profileRes.ok) {\n            const profileRaw: Record<string, unknown> =\n              await profileRes.json()\n            if (cancelled) return\n            const parsed = parseProfile(profileRaw)\n            // Inject display name if not present\n            if (!parsed.name) {\n              parsed.name = buildDisplayName(parsed)\n            }\n            setProfile(parsed)\n          }\n        }\n\n        if (cancelled) return\n        setBapId(resolvedBapId)\n        setCurrentAddress(identityData?.currentAddress ?? null)\n      } catch (err) {\n        if (!cancelled) {\n          setError(\n            err instanceof Error ? err : new Error(\"Failed to load profile\"),\n          )\n        }\n      } finally {\n        if (!cancelled) {\n          setIsLoading(false)\n        }\n      }\n    }\n\n    void fetchProfile()\n\n    return () => {\n      cancelled = true\n    }\n  }, [bapIdProp, address, apiUrl, fetchKey])\n\n  return {\n    bapId,\n    profile,\n    currentAddress,\n    isLoading,\n    error,\n    refetch,\n  }\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/profile-card/use-profile-card.ts"
    }
  ],
  "type": "registry:block"
}