{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "identity-selector",
  "title": "Identity Selector",
  "author": "Satchmo",
  "description": "Dropdown panel for switching between BAP identities with avatar, name, full BAP ID, active indicator, and add identity action",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "card",
    "badge",
    "separator",
    "skeleton",
    "button",
    "https://registry.bigblocks.dev/r/bitcoin-avatar.json"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/identity-selector/index.tsx",
      "content": "\"use client\"\n\nimport {\n  IdentitySelectorUI,\n  type IdentitySelectorUIProps,\n} from \"./identity-selector-ui\"\nimport {\n  useIdentitySelector,\n  type UseIdentitySelectorOptions,\n  type UseIdentitySelectorReturn,\n  type IdentityEntry,\n} from \"./use-identity-selector\"\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport {\n  IdentitySelectorUI,\n  type IdentitySelectorUIProps,\n} from \"./identity-selector-ui\"\nexport {\n  useIdentitySelector,\n  type UseIdentitySelectorOptions,\n  type UseIdentitySelectorReturn,\n  type IdentityEntry,\n} from \"./use-identity-selector\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface IdentitySelectorProps {\n  /** Pre-loaded list of identities (skips API fetch if provided) */\n  identities?: IdentityEntry[]\n  /** BAP ID of the currently active identity */\n  activeBapId?: string\n  /** List of BAP IDs to fetch from the API (used when identities prop is not provided) */\n  bapIds?: string[]\n  /** Called when the user selects a different identity */\n  onSelect?: (bapId: string) => void\n  /** Called when the user clicks \"Add identity\" */\n  onAddIdentity?: () => void\n  /** Whether to show the \"Add identity\" button (default: true) */\n  showAddIdentity?: boolean\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}\n\n// ---------------------------------------------------------------------------\n// Composed component\n// ---------------------------------------------------------------------------\n\n/**\n * A dropdown panel for switching between BAP identities.\n *\n * Shows each identity with avatar, display name, and full BAP ID.\n * The active identity is highlighted with a check icon.\n *\n * Provide either a pre-loaded `identities` array or a `bapIds` list\n * to fetch identity data from the 1sat-stack API.\n *\n * @example\n * ```tsx\n * import { IdentitySelector } from \"@/components/blocks/identity-selector\"\n *\n * // With pre-loaded identities\n * <IdentitySelector\n *   identities={myIdentities}\n *   activeBapId=\"Go8vCHAa4S6AhXKdRp3nT9wJm\"\n *   onSelect={(bapId) => console.log(\"Selected:\", bapId)}\n *   onAddIdentity={() => console.log(\"Add identity\")}\n * />\n *\n * // With API fetch\n * <IdentitySelector\n *   bapIds={[\"Go8vCHAa4S6AhXKdRp3nT9wJm\", \"Hk9wBCDe5F7AiYLmNp2qR8xTs\"]}\n *   onSelect={(bapId) => console.log(\"Selected:\", bapId)}\n * />\n * ```\n */\nexport function IdentitySelector({\n  identities: identitiesProp,\n  activeBapId: activeBapIdProp,\n  bapIds,\n  onSelect,\n  onAddIdentity,\n  showAddIdentity = true,\n  apiUrl,\n  className,\n}: IdentitySelectorProps) {\n  const hook = useIdentitySelector({\n    identities: identitiesProp,\n    activeBapId: activeBapIdProp,\n    bapIds,\n    onSelect,\n    onAddIdentity,\n    apiUrl,\n  })\n\n  return (\n    <IdentitySelectorUI\n      className={className}\n      identities={hook.identities}\n      activeBapId={hook.activeBapId}\n      isLoading={hook.isLoading}\n      error={hook.error}\n      onSelect={hook.selectIdentity}\n      onAddIdentity={hook.addIdentity}\n      showAddIdentity={showAddIdentity}\n    />\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/identity-selector/index.tsx"
    },
    {
      "path": "registry/new-york/blocks/identity-selector/identity-selector-ui.tsx",
      "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Card, CardContent } from \"@/components/ui/card\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { Button } from \"@/components/ui/button\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { BitcoinAvatar } from \"@/registry/new-york/blocks/bitcoin-avatar\"\nimport { Check, Plus, AlertCircle } from \"lucide-react\"\nimport type { IdentityEntry } from \"./use-identity-selector\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface IdentitySelectorUIProps {\n  /** Optional CSS class name */\n  className?: string\n  /** List of available identities */\n  identities: IdentityEntry[]\n  /** Currently active BAP ID */\n  activeBapId: string | null\n  /** Whether identities are loading */\n  isLoading: boolean\n  /** Error if fetch failed */\n  error: Error | null\n  /** Called when an identity row is clicked */\n  onSelect: (bapId: string) => void\n  /** Called when \"Add identity\" is clicked */\n  onAddIdentity: () => void\n  /** Whether to show the \"Add identity\" button (default: true) */\n  showAddIdentity?: boolean\n}\n\n// ---------------------------------------------------------------------------\n// Loading skeleton\n// ---------------------------------------------------------------------------\n\nfunction IdentitySelectorSkeleton({ className }: { className?: string }) {\n  return (\n    <Card className={cn(\"w-full max-w-sm\", className)}>\n      <CardContent className=\"flex flex-col gap-1 p-2\">\n        {[0, 1, 2].map((i) => (\n          <div\n            key={i}\n            className=\"flex items-center gap-3 rounded-md px-3 py-2.5\"\n          >\n            <Skeleton className=\"size-10 shrink-0 rounded-full\" />\n            <div className=\"flex-1 flex flex-col gap-1.5\">\n              <Skeleton className=\"h-4 w-24\" />\n              <Skeleton className=\"h-3 w-40\" />\n            </div>\n          </div>\n        ))}\n      </CardContent>\n    </Card>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// Error state\n// ---------------------------------------------------------------------------\n\nfunction IdentitySelectorError({\n  className,\n  error,\n}: {\n  className?: string\n  error: Error\n}) {\n  return (\n    <Card className={cn(\"w-full max-w-sm\", className)}>\n      <CardContent className=\"flex flex-col items-center gap-2 py-6\">\n        <AlertCircle className=\"size-5 text-destructive\" />\n        <p className=\"text-sm text-muted-foreground text-center\">\n          {error.message}\n        </p>\n      </CardContent>\n    </Card>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// Identity row\n// ---------------------------------------------------------------------------\n\ninterface IdentityRowProps {\n  identity: IdentityEntry\n  isActive: boolean\n  onSelect: (bapId: string) => void\n}\n\nfunction IdentityRow({ identity, isActive, onSelect }: IdentityRowProps) {\n  const displayName = identity.name ?? identity.bapId.slice(0, 8)\n\n  return (\n    <button\n      type=\"button\"\n      className={cn(\n        \"flex w-full items-center gap-3 rounded-md px-3 py-2.5 text-left transition-colors\",\n        isActive\n          ? \"bg-accent text-accent-foreground\"\n          : \"hover:bg-muted/50\",\n      )}\n      onClick={() => onSelect(identity.bapId)}\n      aria-label={`Select identity ${displayName}`}\n      aria-pressed={isActive}\n    >\n      <BitcoinAvatar\n        address={identity.currentAddress}\n        imageUrl={identity.imageUrl ?? undefined}\n        size=\"md\"\n        className=\"shrink-0\"\n      />\n\n      <div className=\"flex-1 min-w-0 flex flex-col gap-0.5\">\n        <p className=\"text-sm font-medium leading-tight truncate\">\n          {displayName}\n        </p>\n        <Badge\n          variant=\"secondary\"\n          className=\"font-mono text-[10px] max-w-full break-all whitespace-normal leading-tight\"\n        >\n          {identity.bapId}\n        </Badge>\n      </div>\n\n      {isActive && (\n        <Check\n          className=\"size-4 shrink-0 text-primary\"\n          aria-label=\"Active identity\"\n        />\n      )}\n    </button>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// Main UI component\n// ---------------------------------------------------------------------------\n\nexport function IdentitySelectorUI({\n  className,\n  identities,\n  activeBapId,\n  isLoading,\n  error,\n  onSelect,\n  onAddIdentity,\n  showAddIdentity = true,\n}: IdentitySelectorUIProps) {\n  if (isLoading) {\n    return <IdentitySelectorSkeleton className={className} />\n  }\n\n  if (error) {\n    return <IdentitySelectorError className={className} error={error} />\n  }\n\n  if (identities.length === 0 && !showAddIdentity) {\n    return (\n      <Card className={cn(\"w-full max-w-sm\", className)}>\n        <CardContent className=\"flex flex-col items-center gap-2 py-6\">\n          <p className=\"text-sm text-muted-foreground\">\n            No identities available\n          </p>\n        </CardContent>\n      </Card>\n    )\n  }\n\n  return (\n    <Card className={cn(\"w-full max-w-sm\", className)}>\n      <CardContent className=\"flex flex-col p-2\">\n        {identities.map((identity) => (\n          <IdentityRow\n            key={identity.bapId}\n            identity={identity}\n            isActive={identity.bapId === activeBapId}\n            onSelect={onSelect}\n          />\n        ))}\n\n        {showAddIdentity && (\n          <>\n            {identities.length > 0 && <Separator className=\"my-1\" />}\n            <Button\n              variant=\"ghost\"\n              size=\"sm\"\n              className=\"w-full justify-start gap-2 text-muted-foreground hover:text-foreground\"\n              onClick={onAddIdentity}\n            >\n              <Plus data-icon=\"inline-start\" />\n              Add identity\n            </Button>\n          </>\n        )}\n      </CardContent>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/identity-selector/identity-selector-ui.tsx"
    },
    {
      "path": "registry/new-york/blocks/identity-selector/use-identity-selector.ts",
      "content": "\"use client\"\n\nimport { useCallback, useEffect, useState } from \"react\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** A single BAP identity entry for the selector */\nexport interface IdentityEntry {\n  /** BAP ID (~27 char base58 string) */\n  bapId: string\n  /** Display name from profile (schema.org name/alternateName) */\n  name: string | null\n  /** Current signing address (used for avatar seed) */\n  currentAddress: string\n  /** Avatar image URL (ord://, b://, https://) if available */\n  imageUrl: string | null\n}\n\n/** BAP identity record returned from the 1sat-stack API */\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\nexport interface UseIdentitySelectorOptions {\n  /** Pre-loaded list of identities (skips API fetch if provided) */\n  identities?: IdentityEntry[]\n  /** BAP ID of the currently active identity */\n  activeBapId?: string\n  /** Called when the user selects a different identity */\n  onSelect?: (bapId: string) => void\n  /** Called when the user clicks \"Add identity\" */\n  onAddIdentity?: () => void\n  /** Base URL for the 1sat-stack API (default: https://api.1sat.app) */\n  apiUrl?: string\n  /** List of BAP IDs to fetch from the API (used when identities prop is not provided) */\n  bapIds?: string[]\n}\n\nexport interface UseIdentitySelectorReturn {\n  /** List of available identities */\n  identities: IdentityEntry[]\n  /** Currently active BAP ID */\n  activeBapId: string | null\n  /** Whether identities are loading */\n  isLoading: boolean\n  /** Error if fetch failed */\n  error: Error | null\n  /** Select an identity by BAP ID */\n  selectIdentity: (bapId: string) => void\n  /** Handle \"Add identity\" action */\n  addIdentity: () => void\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_API_URL = \"https://api.1sat.app\"\n\nfunction extractName(identity: Record<string, unknown> | undefined): string | null {\n  if (!identity) return null\n  if (typeof identity.name === \"string\") return identity.name\n  if (typeof identity.alternateName === \"string\") return identity.alternateName\n  const parts = [\n    typeof identity.givenName === \"string\" ? identity.givenName : null,\n    typeof identity.familyName === \"string\" ? identity.familyName : null,\n  ].filter(Boolean)\n  return parts.length > 0 ? parts.join(\" \") : null\n}\n\nfunction extractImageUrl(identity: Record<string, unknown> | undefined): string | null {\n  if (!identity) return null\n  if (typeof identity.image === \"string\") return identity.image\n  return null\n}\n\nasync function fetchIdentityEntry(\n  bapId: string,\n  apiUrl: string,\n): Promise<IdentityEntry> {\n  const res = await fetch(`${apiUrl}/1sat/bap/identity/get`, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ idKey: bapId }),\n  })\n  if (!res.ok) {\n    throw new Error(`Failed to fetch identity ${bapId}: ${res.status}`)\n  }\n  const data: BapIdentityRecord = await res.json()\n\n  // Try to fetch profile for name/image\n  let profileData: Record<string, unknown> | undefined\n  try {\n    const profileRes = await fetch(\n      `${apiUrl}/1sat/bap/profile/${encodeURIComponent(bapId)}`,\n    )\n    if (profileRes.ok) {\n      const raw: Record<string, unknown> = await profileRes.json()\n      profileData = raw\n    }\n  } catch {\n    // Profile may not exist; use identity data instead\n  }\n\n  const nameSource = profileData ?? data.identity\n  return {\n    bapId: data.idKey,\n    name: extractName(nameSource),\n    currentAddress: data.currentAddress,\n    imageUrl: extractImageUrl(nameSource),\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\nexport function useIdentitySelector({\n  identities: identitiesProp,\n  activeBapId: activeBapIdProp,\n  onSelect,\n  onAddIdentity,\n  apiUrl = DEFAULT_API_URL,\n  bapIds,\n}: UseIdentitySelectorOptions): UseIdentitySelectorReturn {\n  const [identities, setIdentities] = useState<IdentityEntry[]>(\n    identitiesProp ?? [],\n  )\n  const [activeBapId, setActiveBapId] = useState<string | null>(\n    activeBapIdProp ?? null,\n  )\n  const [isLoading, setIsLoading] = useState(false)\n  const [error, setError] = useState<Error | null>(null)\n\n  // Sync externally provided identities\n  useEffect(() => {\n    if (identitiesProp) {\n      setIdentities(identitiesProp)\n    }\n  }, [identitiesProp])\n\n  // Sync externally provided active ID\n  useEffect(() => {\n    if (activeBapIdProp !== undefined) {\n      setActiveBapId(activeBapIdProp)\n    }\n  }, [activeBapIdProp])\n\n  // Fetch identities from API when bapIds are provided and no pre-loaded list\n  useEffect(() => {\n    if (identitiesProp || !bapIds || bapIds.length === 0) return\n\n    let cancelled = false\n    setIsLoading(true)\n    setError(null)\n\n    async function fetchAll() {\n      try {\n        const entries = await Promise.all(\n          // biome-ignore lint: bapIds is checked above\n          bapIds!.map((id) => fetchIdentityEntry(id, apiUrl)),\n        )\n        if (!cancelled) {\n          setIdentities(entries)\n          // Default active to first if none set\n          if (!activeBapIdProp && entries.length > 0) {\n            setActiveBapId(entries[0].bapId)\n          }\n        }\n      } catch (err) {\n        if (!cancelled) {\n          setError(\n            err instanceof Error\n              ? err\n              : new Error(\"Failed to load identities\"),\n          )\n        }\n      } finally {\n        if (!cancelled) {\n          setIsLoading(false)\n        }\n      }\n    }\n\n    void fetchAll()\n\n    return () => {\n      cancelled = true\n    }\n  }, [identitiesProp, bapIds, apiUrl, activeBapIdProp])\n\n  const selectIdentity = useCallback(\n    (bapId: string) => {\n      setActiveBapId(bapId)\n      onSelect?.(bapId)\n    },\n    [onSelect],\n  )\n\n  const addIdentity = useCallback(() => {\n    onAddIdentity?.()\n  }, [onAddIdentity])\n\n  return {\n    identities,\n    activeBapId,\n    isLoading,\n    error,\n    selectIdentity,\n    addIdentity,\n  }\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/identity-selector/use-identity-selector.ts"
    }
  ],
  "type": "registry:block"
}