{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "post-button",
  "title": "Post Button",
  "author": "Satchmo",
  "description": "Compose-dialog button for creating on-chain BSocial posts with default, compact, fab, and inline variants",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "dialog"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/post-button/index.tsx",
      "content": "\"use client\"\n\nimport { type VariantProps } from \"class-variance-authority\"\nimport { PostButtonUI, postButtonVariants } from \"./ui\"\nimport {\n  usePost,\n  type PostResult,\n  type UsePostReturn,\n  type UsePostOptions,\n} from \"./use-post\"\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport {\n  PostButtonUI,\n  postButtonVariants,\n  type PostButtonUIProps,\n} from \"./ui\"\nexport {\n  usePost,\n  type PostResult,\n  type UsePostReturn,\n  type UsePostOptions,\n} from \"./use-post\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface PostButtonProps\n  extends VariantProps<typeof postButtonVariants> {\n  /** Additional CSS classes */\n  className?: string\n  /** Label for the button (default: \"Post\") */\n  label?: string\n  /** Placeholder text for the textarea */\n  placeholder?: string\n  /** Maximum character count (0 = unlimited) */\n  maxLength?: number\n  /** Called to broadcast the post. Receives content string, returns result. */\n  onPost: (content: string) => Promise<PostResult>\n  /** Called after successful post */\n  onPosted?: (result: PostResult) => 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 button that opens a compose dialog for creating on-chain BSocial posts.\n *\n * The `onPost` callback is responsible for building and broadcasting the\n * transaction (e.g. via `BSocial.createPost()` from `@1sat/templates`).\n * This component owns the UI only.\n *\n * @example\n * ```tsx\n * import { PostButton } from \"@/components/blocks/post-button\"\n *\n * <PostButton\n *   onPost={async (content) => {\n *     // build BSocial tx and broadcast\n *     return { txid: \"abc123...\" }\n *   }}\n * />\n * ```\n */\nexport function PostButton({\n  variant = \"default\",\n  className,\n  label = \"Post\",\n  placeholder = \"What's on your mind?\",\n  maxLength = 0,\n  onPost,\n  onPosted,\n  onError,\n  disabled = false,\n}: PostButtonProps) {\n  const hook = usePost({\n    maxLength,\n    onPost,\n    onPosted,\n    onError,\n  })\n\n  return (\n    <PostButtonUI\n      variant={variant}\n      className={className}\n      label={label}\n      placeholder={placeholder}\n      maxLength={maxLength}\n      disabled={disabled}\n      dialogOpen={hook.dialogOpen}\n      content={hook.content}\n      onContentChange={hook.setContent}\n      isPosting={hook.isPosting}\n      error={hook.error}\n      charCount={hook.charCount}\n      overLimit={hook.overLimit}\n      canSubmit={hook.canSubmit}\n      onOpen={hook.handleOpen}\n      onClose={hook.handleClose}\n      onSubmit={hook.handleSubmit}\n    />\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/post-button/index.tsx"
    },
    {
      "path": "registry/new-york/blocks/post-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 {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\"\nimport { Loader2, MessageSquarePlus, Pencil, Plus, Send } from \"lucide-react\"\n\n// ---------------------------------------------------------------------------\n// Variant definitions\n// ---------------------------------------------------------------------------\n\nexport const postButtonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"rounded-md bg-primary text-primary-foreground shadow hover:bg-primary/90 px-4 py-2 text-sm\",\n        compact:\n          \"rounded-md bg-primary text-primary-foreground shadow hover:bg-primary/90 h-9 px-3 text-sm\",\n        fab: \"fixed bottom-6 right-6 z-50 size-14 rounded-full bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 hover:shadow-xl transition-all\",\n        inline:\n          \"rounded-md border border-input bg-background text-foreground shadow-sm hover:bg-accent hover:text-accent-foreground px-3 py-1.5 text-xs\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n)\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface PostButtonUIProps\n  extends VariantProps<typeof postButtonVariants> {\n  /** Additional CSS classes */\n  className?: string\n  /** Label for the button (default: \"Post\") */\n  label?: string\n  /** Placeholder text for the textarea */\n  placeholder?: string\n  /** Maximum character count (0 = unlimited) */\n  maxLength?: number\n  /** Disable the trigger button */\n  disabled?: boolean\n  /** Whether the compose dialog is open */\n  dialogOpen: boolean\n  /** Current content of the textarea */\n  content: string\n  /** Set the content of the textarea */\n  onContentChange: (content: string) => void\n  /** Whether a post is being submitted */\n  isPosting: boolean\n  /** Error message string, if present */\n  error: string | null\n  /** Current character count */\n  charCount: number\n  /** Whether the content exceeds the max length */\n  overLimit: boolean\n  /** Whether the post can be submitted */\n  canSubmit: boolean\n  /** Open the compose dialog */\n  onOpen: () => void\n  /** Close the compose dialog */\n  onClose: () => void\n  /** Submit the post */\n  onSubmit: () => void\n}\n\n// ---------------------------------------------------------------------------\n// Icon per variant\n// ---------------------------------------------------------------------------\n\nfunction VariantIcon({ variant }: { variant: PostButtonUIProps[\"variant\"] }) {\n  switch (variant) {\n    case \"compact\":\n      return <Pencil className=\"size-4\" aria-hidden=\"true\" />\n    case \"fab\":\n      return <Plus className=\"size-6\" aria-hidden=\"true\" />\n    case \"inline\":\n      return <Pencil className=\"size-3.5\" aria-hidden=\"true\" />\n    default:\n      return <MessageSquarePlus className=\"size-4\" aria-hidden=\"true\" />\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nexport function PostButtonUI({\n  variant = \"default\",\n  className,\n  label = \"Post\",\n  placeholder = \"What's on your mind?\",\n  maxLength = 0,\n  disabled = false,\n  dialogOpen,\n  content,\n  onContentChange,\n  isPosting,\n  error,\n  charCount,\n  overLimit,\n  canSubmit,\n  onOpen,\n  onClose,\n  onSubmit,\n}: PostButtonUIProps) {\n  const isFab = variant === \"fab\"\n  const isCompact = variant === \"compact\"\n\n  return (\n    <>\n      <button\n        type=\"button\"\n        className={cn(postButtonVariants({ variant }), className)}\n        onClick={onOpen}\n        disabled={disabled}\n        aria-label={isFab ? \"Create post\" : undefined}\n      >\n        <VariantIcon variant={variant} />\n        {!isFab && !isCompact && <span>{label}</span>}\n        {isCompact && <span>{label}</span>}\n      </button>\n\n      <Dialog open={dialogOpen} onOpenChange={(open) => !open && onClose()}>\n        <DialogContent className=\"sm:max-w-lg\">\n          <DialogHeader>\n            <DialogTitle>Create Post</DialogTitle>\n            <DialogDescription>\n              Write your post. It will be inscribed on-chain using the BSocial\n              protocol.\n            </DialogDescription>\n          </DialogHeader>\n\n          <div className=\"flex flex-col gap-3 py-2\">\n            <textarea\n              className={cn(\n                \"flex min-h-[120px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 resize-none\",\n                overLimit && \"border-destructive focus-visible:ring-destructive\",\n              )}\n              placeholder={placeholder}\n              value={content}\n              onChange={(e) => onContentChange(e.target.value)}\n              disabled={isPosting}\n              aria-label=\"Post content\"\n            />\n\n            <div className=\"flex items-center justify-between\">\n              {maxLength > 0 ? (\n                <p\n                  className={cn(\n                    \"text-xs text-muted-foreground\",\n                    overLimit && \"text-destructive font-medium\",\n                  )}\n                >\n                  {charCount}/{maxLength}\n                </p>\n              ) : (\n                <span />\n              )}\n            </div>\n\n            {error && (\n              <p className=\"text-sm text-destructive\" role=\"alert\">\n                {error}\n              </p>\n            )}\n          </div>\n\n          <DialogFooter>\n            <Button\n              variant=\"outline\"\n              onClick={onClose}\n              disabled={isPosting}\n            >\n              Cancel\n            </Button>\n            <Button\n              onClick={onSubmit}\n              disabled={!canSubmit}\n              aria-busy={isPosting}\n            >\n              {isPosting ? (\n                <>\n                  <Loader2 className=\"animate-spin\" data-icon=\"inline-start\" />\n                  Posting...\n                </>\n              ) : (\n                <>\n                  <Send data-icon=\"inline-start\" />\n                  Post\n                </>\n              )}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </>\n  )\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/post-button/ui.tsx"
    },
    {
      "path": "registry/new-york/blocks/post-button/use-post.ts",
      "content": "import { useCallback, useState } from \"react\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface PostResult {\n  /** Transaction ID of the on-chain post */\n  txid?: string\n  /** Raw transaction hex */\n  rawtx?: string\n  /** Error message if posting failed */\n  error?: string\n}\n\nexport interface UsePostOptions {\n  /** Maximum character count (0 = unlimited) */\n  maxLength?: number\n  /** Called to broadcast the post. Receives content string, returns result. */\n  onPost: (content: string) => Promise<PostResult>\n  /** Called after successful post */\n  onPosted?: (result: PostResult) => void\n  /** Called on error */\n  onError?: (error: Error) => void\n}\n\nexport interface UsePostReturn {\n  /** Whether the compose dialog is open */\n  dialogOpen: boolean\n  /** Current content of the textarea */\n  content: string\n  /** Set the content of the textarea */\n  setContent: (content: string) => void\n  /** Whether a post is being submitted */\n  isPosting: boolean\n  /** Error message string, if present */\n  error: string | null\n  /** Current character count */\n  charCount: number\n  /** Whether the content exceeds the max length */\n  overLimit: boolean\n  /** Whether the post can be submitted */\n  canSubmit: boolean\n  /** Open the compose dialog */\n  handleOpen: () => void\n  /** Close the compose dialog (resets state) */\n  handleClose: () => void\n  /** Submit the post */\n  handleSubmit: () => Promise<void>\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\nexport function usePost({\n  maxLength = 0,\n  onPost,\n  onPosted,\n  onError,\n}: UsePostOptions): UsePostReturn {\n  const [dialogOpen, setDialogOpen] = useState(false)\n  const [content, setContent] = useState(\"\")\n  const [isPosting, setIsPosting] = useState(false)\n  const [error, setError] = useState<string | null>(null)\n\n  const charCount = content.length\n  const overLimit = maxLength > 0 && charCount > maxLength\n  const canSubmit = content.trim().length > 0 && !overLimit && !isPosting\n\n  const handleOpen = useCallback(() => {\n    setDialogOpen(true)\n    setError(null)\n  }, [])\n\n  const handleClose = useCallback(() => {\n    if (!isPosting) {\n      setDialogOpen(false)\n      setContent(\"\")\n      setError(null)\n    }\n  }, [isPosting])\n\n  const handleSubmit = useCallback(async () => {\n    if (!canSubmit) return\n\n    setIsPosting(true)\n    setError(null)\n\n    try {\n      const result = await onPost(content.trim())\n\n      if (result.error) {\n        setError(result.error)\n        onError?.(new Error(result.error))\n      } else {\n        onPosted?.(result)\n        setDialogOpen(false)\n        setContent(\"\")\n      }\n    } catch (err) {\n      const msg = err instanceof Error ? err.message : \"Failed to create post\"\n      setError(msg)\n      onError?.(err instanceof Error ? err : new Error(msg))\n    } finally {\n      setIsPosting(false)\n    }\n  }, [canSubmit, content, onPost, onPosted, onError])\n\n  return {\n    dialogOpen,\n    content,\n    setContent,\n    isPosting,\n    error,\n    charCount,\n    overLimit,\n    canSubmit,\n    handleOpen,\n    handleClose,\n    handleSubmit,\n  }\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/post-button/use-post.ts"
    }
  ],
  "type": "registry:block"
}