{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "follow-button",
  "title": "Follow Button",
  "author": "Satchmo",
  "description": "Three-state follow/following/unfollow toggle for BSocial identities with hover-to-unfollow UX and default, compact, pill variants",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/follow-button/index.tsx",
      "content": "\"use client\"\n\nimport { type VariantProps } from \"class-variance-authority\"\nimport { FollowButtonUI, followButtonVariants } from \"./ui\"\nimport {\n  useFollow,\n  type FollowResult,\n  type FollowState,\n  type UseFollowReturn,\n  type UseFollowOptions,\n} from \"./use-follow\"\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport {\n  FollowButtonUI,\n  followButtonVariants,\n  type FollowButtonUIProps,\n} from \"./ui\"\nexport {\n  useFollow,\n  type FollowResult,\n  type FollowState,\n  type UseFollowReturn,\n  type UseFollowOptions,\n} from \"./use-follow\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface FollowButtonProps\n  extends VariantProps<typeof followButtonVariants> {\n  /** Additional CSS classes */\n  className?: string\n  /** BAP identity key of the user to follow */\n  bapId: string\n  /** Whether the current user is already following this user */\n  isFollowing?: boolean\n  /** Called to broadcast a follow action */\n  onFollow: (bapId: string) => Promise<FollowResult>\n  /** Called to broadcast an unfollow action */\n  onUnfollow?: (bapId: string) => Promise<FollowResult>\n  /** Called after a successful follow/unfollow */\n  onToggled?: (following: boolean, result: FollowResult) => void\n  /** Called on error */\n  onError?: (error: Error) => void\n  /** Disable the button */\n  disabled?: boolean\n  /** Labels for the three visual states */\n  labels?: {\n    follow?: string\n    following?: string\n    unfollow?: string\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Composed component\n// ---------------------------------------------------------------------------\n\n/**\n * A follow/unfollow toggle button for BSocial identities.\n *\n * Renders three visual states:\n * - **Follow** (idle) -- primary CTA to follow a user\n * - **Following** (confirmed) -- subdued indicator that the user is followed\n * - **Unfollow** (hover on \"Following\") -- destructive action on hover\n *\n * Delegates transaction building to `onFollow`/`onUnfollow` callbacks.\n *\n * @example\n * ```tsx\n * import { FollowButton } from \"@/components/blocks/follow-button\"\n *\n * <FollowButton\n *   bapId=\"02abc...\"\n *   isFollowing={false}\n *   onFollow={async (bapId) => {\n *     // build BSocial follow tx\n *     return { txid: \"abc123...\" }\n *   }}\n * />\n * ```\n */\nexport function FollowButton({\n  variant = \"default\",\n  className,\n  bapId,\n  isFollowing = false,\n  onFollow,\n  onUnfollow,\n  onToggled,\n  onError,\n  disabled = false,\n  labels = {},\n}: FollowButtonProps) {\n  const hook = useFollow({\n    bapId,\n    isFollowing,\n    onFollow,\n    onUnfollow,\n    onToggled,\n    onError,\n    labels,\n  })\n\n  return (\n    <FollowButtonUI\n      variant={variant}\n      className={className}\n      bapId={bapId}\n      following={hook.following}\n      showUnfollow={hook.showUnfollow}\n      isLoading={hook.isLoading}\n      currentLabel={hook.currentLabel}\n      onClick={hook.handleClick}\n      onMouseEnter={() => hook.setIsHovering(true)}\n      onMouseLeave={() => hook.setIsHovering(false)}\n      disabled={disabled}\n    />\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/follow-button/index.tsx"
    },
    {
      "path": "registry/new-york/blocks/follow-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 { Loader2, UserMinus, UserPlus } from \"lucide-react\"\n\n// ---------------------------------------------------------------------------\n// Variant definitions\n// ---------------------------------------------------------------------------\n\nexport const followButtonVariants = 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        pill: \"rounded-full px-5 py-2 text-sm shadow-sm\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n)\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface FollowButtonUIProps\n  extends VariantProps<typeof followButtonVariants> {\n  /** Additional CSS classes */\n  className?: string\n  /** BAP identity key of the user (for aria-label) */\n  bapId: string\n  /** Whether the current user is following the target */\n  following: boolean\n  /** Whether the unfollow UI should show (hover state) */\n  showUnfollow: boolean\n  /** Whether a follow/unfollow action is in progress */\n  isLoading: boolean\n  /** The current label to display */\n  currentLabel: string\n  /** Handle the follow/unfollow click */\n  onClick: () => void\n  /** Handle mouse enter */\n  onMouseEnter: () => void\n  /** Handle mouse leave */\n  onMouseLeave: () => void\n  /** Disable the button */\n  disabled?: boolean\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nexport function FollowButtonUI({\n  variant = \"default\",\n  className,\n  bapId,\n  following,\n  showUnfollow,\n  isLoading,\n  currentLabel,\n  onClick,\n  onMouseEnter,\n  onMouseLeave,\n  disabled = false,\n}: FollowButtonUIProps) {\n  return (\n    <Button\n      variant=\"ghost\"\n      size={variant === \"compact\" ? \"sm\" : \"default\"}\n      className={cn(\n        followButtonVariants({ variant }),\n        // Follow state (not following)\n        !following &&\n          \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        // Following state (no hover)\n        following &&\n          !showUnfollow &&\n          \"border border-input bg-background text-foreground hover:bg-accent/50\",\n        // Unfollow state (hover while following)\n        showUnfollow &&\n          \"border border-destructive/30 bg-destructive/5 text-destructive hover:bg-destructive/10\",\n        className,\n      )}\n      onClick={onClick}\n      onMouseEnter={onMouseEnter}\n      onMouseLeave={onMouseLeave}\n      disabled={disabled || isLoading}\n      aria-label={following ? `Unfollow ${bapId}` : `Follow ${bapId}`}\n      aria-pressed={following}\n    >\n      {isLoading ? (\n        <Loader2 className=\"animate-spin\" data-icon=\"inline-start\" aria-hidden=\"true\" />\n      ) : showUnfollow ? (\n        <UserMinus data-icon=\"inline-start\" aria-hidden=\"true\" />\n      ) : !following ? (\n        <UserPlus data-icon=\"inline-start\" aria-hidden=\"true\" />\n      ) : null}\n      <span>{currentLabel}</span>\n    </Button>\n  )\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/follow-button/ui.tsx"
    },
    {
      "path": "registry/new-york/blocks/follow-button/use-follow.ts",
      "content": "import { useCallback, useState } from \"react\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type FollowState = \"idle\" | \"following\" | \"unfollowing\"\n\nexport interface FollowResult {\n  /** Transaction ID of the follow/unfollow action */\n  txid?: string\n  /** Raw transaction hex */\n  rawtx?: string\n  /** Error message if the action failed */\n  error?: string\n}\n\nexport interface UseFollowOptions {\n  /** BAP identity key of the user to follow */\n  bapId: string\n  /** Whether the current user is already following this user */\n  isFollowing?: boolean\n  /** Called to broadcast a follow action */\n  onFollow: (bapId: string) => Promise<FollowResult>\n  /** Called to broadcast an unfollow action */\n  onUnfollow?: (bapId: string) => Promise<FollowResult>\n  /** Called after a successful follow/unfollow */\n  onToggled?: (following: boolean, result: FollowResult) => void\n  /** Called on error */\n  onError?: (error: Error) => void\n}\n\nexport interface UseFollowReturn {\n  /** Whether the current user is following the target */\n  following: boolean\n  /** Whether the user is hovering over the button */\n  isHovering: boolean\n  /** Set the hovering state */\n  setIsHovering: (hovering: boolean) => void\n  /** Current action state */\n  actionState: FollowState\n  /** Whether a follow/unfollow action is in progress */\n  isLoading: boolean\n  /** Whether the unfollow UI should show */\n  showUnfollow: boolean\n  /** The current label to display */\n  currentLabel: string\n  /** Handle the follow/unfollow click */\n  handleClick: () => Promise<void>\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\nexport function useFollow({\n  bapId,\n  isFollowing: initialFollowing = false,\n  onFollow,\n  onUnfollow,\n  onToggled,\n  onError,\n  labels = {},\n}: UseFollowOptions & {\n  labels?: {\n    follow?: string\n    following?: string\n    unfollow?: string\n  }\n}): UseFollowReturn {\n  const [following, setFollowing] = useState(initialFollowing)\n  const [isHovering, setIsHovering] = useState(false)\n  const [actionState, setActionState] = useState<FollowState>(\"idle\")\n\n  const followLabel = labels.follow ?? \"Follow\"\n  const followingLabel = labels.following ?? \"Following\"\n  const unfollowLabel = labels.unfollow ?? \"Unfollow\"\n\n  const isLoading = actionState === \"following\" || actionState === \"unfollowing\"\n\n  const showUnfollow = following && isHovering && !!onUnfollow\n\n  const currentLabel = isLoading\n    ? following\n      ? \"Unfollowing...\"\n      : \"Following...\"\n    : showUnfollow\n      ? unfollowLabel\n      : following\n        ? followingLabel\n        : followLabel\n\n  const handleClick = useCallback(async () => {\n    if (isLoading) return\n\n    const wasFollowing = following\n\n    if (wasFollowing) {\n      if (!onUnfollow) return\n\n      setActionState(\"unfollowing\")\n\n      try {\n        const result = await onUnfollow(bapId)\n\n        if (result.error) {\n          onError?.(new Error(result.error))\n        } else {\n          setFollowing(false)\n          onToggled?.(false, result)\n        }\n      } catch (err) {\n        const error = err instanceof Error ? err : new Error(\"Failed to unfollow\")\n        onError?.(error)\n      } finally {\n        setActionState(\"idle\")\n      }\n    } else {\n      setActionState(\"following\")\n\n      try {\n        const result = await onFollow(bapId)\n\n        if (result.error) {\n          onError?.(new Error(result.error))\n        } else {\n          setFollowing(true)\n          onToggled?.(true, result)\n        }\n      } catch (err) {\n        const error = err instanceof Error ? err : new Error(\"Failed to follow\")\n        onError?.(error)\n      } finally {\n        setActionState(\"idle\")\n      }\n    }\n  }, [isLoading, following, bapId, onFollow, onUnfollow, onToggled, onError])\n\n  return {\n    following,\n    isHovering,\n    setIsHovering,\n    actionState,\n    isLoading,\n    showUnfollow,\n    currentLabel,\n    handleClick,\n  }\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/follow-button/use-follow.ts"
    }
  ],
  "type": "registry:block"
}