{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "friend-button",
  "title": "Friend Button",
  "author": "Satchmo",
  "description": "Four-state friend request button (Add Friend, Pending, Accept/Decline, Friends) using BSocial mutual follows and Type-42 key derivation",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/friend-button/index.tsx",
      "content": "\"use client\"\n\nimport { type VariantProps } from \"class-variance-authority\"\nimport { FriendButtonUI, friendButtonVariants } from \"./ui\"\nimport {\n  useFriend,\n  type FriendResult,\n  type FriendshipStatus,\n  type UseFriendReturn,\n  type UseFriendOptions,\n} from \"./use-friend\"\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport {\n  FriendButtonUI,\n  friendButtonVariants,\n  type FriendButtonUIProps,\n} from \"./ui\"\nexport {\n  useFriend,\n  type FriendResult,\n  type FriendshipStatus,\n  type UseFriendReturn,\n  type UseFriendOptions,\n} from \"./use-friend\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface FriendButtonProps\n  extends VariantProps<typeof friendButtonVariants> {\n  /** Additional CSS classes */\n  className?: string\n  /** Identity key of the other user */\n  identityKey: string\n  /** Current friendship status */\n  status: FriendshipStatus\n  /** Called to send a friend request (creates a BSocial follow) */\n  onAddFriend: (identityKey: string) => Promise<FriendResult>\n  /** Called to accept an incoming request (creates a mutual follow + derives shared key) */\n  onAccept?: (identityKey: string) => Promise<FriendResult>\n  /** Called to decline an incoming request */\n  onDecline?: (identityKey: string) => Promise<FriendResult>\n  /** Called to remove an existing friend */\n  onRemove?: (identityKey: string) => Promise<FriendResult>\n  /** Called after any successful action with the new status */\n  onStatusChange?: (newStatus: FriendshipStatus, result: FriendResult) => void\n  /** Called on error */\n  onError?: (error: Error) => void\n  /** Disable the button */\n  disabled?: boolean\n}\n\n// ---------------------------------------------------------------------------\n// Composed component\n// ---------------------------------------------------------------------------\n\n/**\n * A friend request button with four states: Add Friend, Pending (sent),\n * Accept/Decline (received), and Friends.\n *\n * Uses BSocial follow protocol for friend requests. When both users follow\n * each other, they become \"friends\" and can derive shared encryption keys\n * via `getFriendPublicKey()` from `@1sat/actions`.\n *\n * @example\n * ```tsx\n * import { FriendButton } from \"@/components/blocks/friend-button\"\n *\n * <FriendButton\n *   identityKey=\"02abc...\"\n *   status=\"none\"\n *   onAddFriend={async (id) => {\n *     // create BSocial follow tx\n *     return { txid: \"abc123...\" }\n *   }}\n *   onAccept={async (id) => {\n *     // create mutual follow + derive shared key\n *     return { txid: \"def456...\", friendPublicKey: \"03xyz...\" }\n *   }}\n * />\n * ```\n */\nexport function FriendButton({\n  variant = \"default\",\n  className,\n  identityKey,\n  status,\n  onAddFriend,\n  onAccept,\n  onDecline,\n  onRemove,\n  onStatusChange,\n  onError,\n  disabled = false,\n}: FriendButtonProps) {\n  const hook = useFriend({\n    identityKey,\n    status,\n    onAddFriend,\n    onAccept,\n    onDecline,\n    onRemove,\n    onStatusChange,\n    onError,\n  })\n\n  return (\n    <FriendButtonUI\n      variant={variant}\n      className={className}\n      identityKey={identityKey}\n      currentStatus={hook.currentStatus}\n      isLoading={hook.isLoading}\n      loadingAction={hook.loadingAction}\n      isHovering={hook.isHovering}\n      onMouseEnter={() => hook.setIsHovering(true)}\n      onMouseLeave={() => hook.setIsHovering(false)}\n      onAdd={hook.handleAdd}\n      onAccept={hook.handleAccept}\n      onDecline={hook.handleDecline}\n      onRemove={hook.handleRemove}\n      hasAccept={hook.hasAccept}\n      hasDecline={hook.hasDecline}\n      hasRemove={hook.hasRemove}\n      disabled={disabled}\n    />\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/friend-button/index.tsx"
    },
    {
      "path": "registry/new-york/blocks/friend-button/ui.tsx",
      "content": "\"use client\"\n\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { Check, Loader2, UserCheck, UserPlus, UserX, X } from \"lucide-react\"\nimport type { FriendshipStatus } from \"./use-friend\"\n\n// ---------------------------------------------------------------------------\n// Variant definitions\n// ---------------------------------------------------------------------------\n\nexport const friendButtonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 font-medium transition-all\",\n  {\n    variants: {\n      variant: {\n        default: \"rounded-md px-4 py-2 text-sm shadow-sm\",\n        compact: \"rounded-md px-3 py-1.5 text-xs shadow-sm\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n)\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface FriendButtonUIProps\n  extends VariantProps<typeof friendButtonVariants> {\n  /** Additional CSS classes */\n  className?: string\n  /** Identity key of the other user (for aria-label) */\n  identityKey: string\n  /** Current friendship status */\n  currentStatus: FriendshipStatus\n  /** Whether an action is in progress */\n  isLoading: boolean\n  /** Which action is currently loading */\n  loadingAction: string | null\n  /** Whether the user is hovering (for friends state) */\n  isHovering: boolean\n  /** Handle mouse enter */\n  onMouseEnter: () => void\n  /** Handle mouse leave */\n  onMouseLeave: () => void\n  /** Handle add friend */\n  onAdd: () => void\n  /** Handle accept */\n  onAccept: () => void\n  /** Handle decline */\n  onDecline: () => void\n  /** Handle remove */\n  onRemove: () => void\n  /** Whether onAccept is available */\n  hasAccept: boolean\n  /** Whether onDecline is available */\n  hasDecline: boolean\n  /** Whether onRemove is available */\n  hasRemove: boolean\n  /** Disable the button */\n  disabled?: boolean\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nexport function FriendButtonUI({\n  variant = \"default\",\n  className,\n  identityKey,\n  currentStatus,\n  isLoading,\n  loadingAction,\n  isHovering,\n  onMouseEnter,\n  onMouseLeave,\n  onAdd,\n  onAccept,\n  onDecline,\n  onRemove,\n  hasAccept,\n  hasDecline,\n  hasRemove,\n  disabled = false,\n}: FriendButtonUIProps) {\n  const buttonSize = variant === \"compact\" ? \"sm\" : \"default\" as const\n\n  // --- None: \"Add Friend\" ---\n  if (currentStatus === \"none\") {\n    return (\n      <Button\n        size={buttonSize}\n        className={cn(\n          friendButtonVariants({ variant }),\n          \"bg-primary text-primary-foreground hover:bg-primary/90\",\n          className,\n        )}\n        onClick={onAdd}\n        disabled={disabled || isLoading}\n        aria-label={`Send friend request to ${identityKey}`}\n        aria-busy={isLoading}\n      >\n        {isLoading ? (\n          <>\n            <Loader2 className=\"animate-spin\" data-icon=\"inline-start\" aria-hidden=\"true\" />\n            <span>Sending...</span>\n          </>\n        ) : (\n          <>\n            <UserPlus data-icon=\"inline-start\" aria-hidden=\"true\" />\n            <span>Add Friend</span>\n          </>\n        )}\n      </Button>\n    )\n  }\n\n  // --- Pending Sent: \"Pending\" ---\n  if (currentStatus === \"pending-sent\") {\n    return (\n      <Button\n        variant=\"outline\"\n        size={buttonSize}\n        className={cn(\n          friendButtonVariants({ variant }),\n          \"bg-muted text-muted-foreground cursor-default\",\n          className,\n        )}\n        disabled\n        aria-label=\"Friend request pending\"\n      >\n        <Loader2 className=\"animate-spin opacity-50\" data-icon=\"inline-start\" aria-hidden=\"true\" />\n        <span>Pending</span>\n      </Button>\n    )\n  }\n\n  // --- Pending Received: \"Accept\" + \"Decline\" ---\n  if (currentStatus === \"pending-received\") {\n    return (\n      <div className=\"inline-flex items-center gap-2\">\n        {hasAccept && (\n          <Button\n            size={buttonSize}\n            className={cn(\"gap-2\", className)}\n            onClick={onAccept}\n            disabled={disabled || isLoading}\n            aria-label={`Accept friend request from ${identityKey}`}\n            aria-busy={loadingAction === \"accept\"}\n          >\n            {loadingAction === \"accept\" ? (\n              <>\n                <Loader2 className=\"animate-spin\" data-icon=\"inline-start\" aria-hidden=\"true\" />\n                Accepting...\n              </>\n            ) : (\n              <>\n                <Check data-icon=\"inline-start\" aria-hidden=\"true\" />\n                Accept\n              </>\n            )}\n          </Button>\n        )}\n\n        {hasDecline && (\n          <Button\n            variant=\"outline\"\n            size={buttonSize}\n            className=\"gap-2\"\n            onClick={onDecline}\n            disabled={disabled || isLoading}\n            aria-label={`Decline friend request from ${identityKey}`}\n            aria-busy={loadingAction === \"decline\"}\n          >\n            {loadingAction === \"decline\" ? (\n              <Loader2 className=\"animate-spin\" data-icon=\"inline-start\" aria-hidden=\"true\" />\n            ) : (\n              <X data-icon=\"inline-start\" aria-hidden=\"true\" />\n            )}\n            Decline\n          </Button>\n        )}\n      </div>\n    )\n  }\n\n  // --- Friends: show \"Friends\" badge, with hover to \"Remove\" ---\n  return (\n    <Button\n      variant=\"ghost\"\n      size={buttonSize}\n      className={cn(\n        friendButtonVariants({ variant }),\n        !isHovering &&\n          \"border border-primary/20 bg-primary/5 text-primary\",\n        isHovering &&\n          hasRemove &&\n          \"border border-destructive/30 bg-destructive/5 text-destructive hover:bg-destructive/10\",\n        isHovering && !hasRemove && \"border border-primary/20 bg-primary/5 text-primary\",\n        className,\n      )}\n      onClick={hasRemove ? onRemove : undefined}\n      onMouseEnter={onMouseEnter}\n      onMouseLeave={onMouseLeave}\n      disabled={disabled || isLoading || !hasRemove}\n      aria-label={\n        isHovering && hasRemove\n          ? `Remove friend ${identityKey}`\n          : `Friends with ${identityKey}`\n      }\n    >\n      {isLoading ? (\n        <>\n          <Loader2 className=\"animate-spin\" data-icon=\"inline-start\" aria-hidden=\"true\" />\n          <span>Removing...</span>\n        </>\n      ) : isHovering && hasRemove ? (\n        <>\n          <UserX data-icon=\"inline-start\" aria-hidden=\"true\" />\n          <span>Remove</span>\n        </>\n      ) : (\n        <>\n          <UserCheck data-icon=\"inline-start\" aria-hidden=\"true\" />\n          <span>Friends</span>\n        </>\n      )}\n    </Button>\n  )\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/friend-button/ui.tsx"
    },
    {
      "path": "registry/new-york/blocks/friend-button/use-friend.ts",
      "content": "import { useCallback, useState } from \"react\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * The friendship state from the current user's perspective:\n * - `none` -- no relationship\n * - `pending-sent` -- current user sent a request (waiting for acceptance)\n * - `pending-received` -- another user sent a request (can accept or decline)\n * - `friends` -- mutual follow established\n */\nexport type FriendshipStatus =\n  | \"none\"\n  | \"pending-sent\"\n  | \"pending-received\"\n  | \"friends\"\n\nexport interface FriendResult {\n  /** Transaction ID of the action */\n  txid?: string\n  /** Derived public key for encrypted messaging (returned on accept) */\n  friendPublicKey?: string\n  /** Raw transaction hex */\n  rawtx?: string\n  /** Error message if the action failed */\n  error?: string\n}\n\nexport interface UseFriendOptions {\n  /** Identity key of the other user */\n  identityKey: string\n  /** Current friendship status */\n  status: FriendshipStatus\n  /** Called to send a friend request (creates a BSocial follow) */\n  onAddFriend: (identityKey: string) => Promise<FriendResult>\n  /** Called to accept an incoming request (creates a mutual follow + derives shared key) */\n  onAccept?: (identityKey: string) => Promise<FriendResult>\n  /** Called to decline an incoming request */\n  onDecline?: (identityKey: string) => Promise<FriendResult>\n  /** Called to remove an existing friend */\n  onRemove?: (identityKey: string) => Promise<FriendResult>\n  /** Called after any successful action with the new status */\n  onStatusChange?: (newStatus: FriendshipStatus, result: FriendResult) => void\n  /** Called on error */\n  onError?: (error: Error) => void\n}\n\nexport interface UseFriendReturn {\n  /** Current friendship status */\n  currentStatus: FriendshipStatus\n  /** Whether an action is in progress */\n  isLoading: boolean\n  /** Which action is currently loading */\n  loadingAction: string | null\n  /** Whether the user is hovering (for friends state) */\n  isHovering: boolean\n  /** Set the hovering state */\n  setIsHovering: (hovering: boolean) => void\n  /** Execute an action (add, accept, decline, remove) */\n  executeAction: (\n    action: (id: string) => Promise<FriendResult>,\n    actionName: string,\n    newStatus: FriendshipStatus,\n  ) => Promise<void>\n  /** Whether onAccept is available */\n  hasAccept: boolean\n  /** Whether onDecline is available */\n  hasDecline: boolean\n  /** Whether onRemove is available */\n  hasRemove: boolean\n  /** Convenience handlers */\n  handleAdd: () => void\n  handleAccept: () => void\n  handleDecline: () => void\n  handleRemove: () => void\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\nexport function useFriend({\n  identityKey,\n  status,\n  onAddFriend,\n  onAccept,\n  onDecline,\n  onRemove,\n  onStatusChange,\n  onError,\n}: UseFriendOptions): UseFriendReturn {\n  const [currentStatus, setCurrentStatus] = useState<FriendshipStatus>(status)\n  const [isLoading, setIsLoading] = useState(false)\n  const [loadingAction, setLoadingAction] = useState<string | null>(null)\n  const [isHovering, setIsHovering] = useState(false)\n\n  const executeAction = useCallback(\n    async (\n      action: (id: string) => Promise<FriendResult>,\n      actionName: string,\n      newStatus: FriendshipStatus,\n    ) => {\n      setIsLoading(true)\n      setLoadingAction(actionName)\n\n      try {\n        const result = await action(identityKey)\n\n        if (result.error) {\n          onError?.(new Error(result.error))\n        } else {\n          setCurrentStatus(newStatus)\n          onStatusChange?.(newStatus, result)\n        }\n      } catch (err) {\n        const error = err instanceof Error ? err : new Error(`Failed to ${actionName}`)\n        onError?.(error)\n      } finally {\n        setIsLoading(false)\n        setLoadingAction(null)\n      }\n    },\n    [identityKey, onStatusChange, onError],\n  )\n\n  const handleAdd = useCallback(() => {\n    void executeAction(onAddFriend, \"add\", \"pending-sent\")\n  }, [executeAction, onAddFriend])\n\n  const handleAccept = useCallback(() => {\n    if (onAccept) {\n      void executeAction(onAccept, \"accept\", \"friends\")\n    }\n  }, [executeAction, onAccept])\n\n  const handleDecline = useCallback(() => {\n    if (onDecline) {\n      void executeAction(onDecline, \"decline\", \"none\")\n    }\n  }, [executeAction, onDecline])\n\n  const handleRemove = useCallback(() => {\n    if (onRemove) {\n      void executeAction(onRemove, \"remove\", \"none\")\n    }\n  }, [executeAction, onRemove])\n\n  return {\n    currentStatus,\n    isLoading,\n    loadingAction,\n    isHovering,\n    setIsHovering,\n    executeAction,\n    hasAccept: !!onAccept,\n    hasDecline: !!onDecline,\n    hasRemove: !!onRemove,\n    handleAdd,\n    handleAccept,\n    handleDecline,\n    handleRemove,\n  }\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/friend-button/use-friend.ts"
    }
  ],
  "type": "registry:block"
}