{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "opns-manager",
  "title": "OpNS Manager",
  "author": "Satchmo <https://bigblocks.dev>",
  "description": "OpNS name management block for listing owned names and registering or deregistering identity key bindings via @1sat/actions opns module",
  "dependencies": [
    "@1sat/react",
    "@1sat/actions",
    "lucide-react"
  ],
  "registryDependencies": [
    "badge",
    "button",
    "card",
    "dialog",
    "separator",
    "skeleton"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/opns-manager/index.tsx",
      "content": "\"use client\"\n\nimport { OpnsManagerUI } from \"./opns-manager-ui\"\nimport {\n  useOpnsManager,\n  type UseOpnsManagerOptions,\n  type OpnsName,\n} from \"./use-opns-manager\"\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport {\n  OpnsManagerUI,\n  type OpnsManagerUIProps,\n  type OpnsNameDisplay,\n  type OpnsOperationResult,\n} from \"./opns-manager-ui\"\nexport {\n  useOpnsManager,\n  type UseOpnsManagerOptions,\n  type UseOpnsManagerReturn,\n  type OpnsName,\n} from \"./use-opns-manager\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface OpnsManagerProps {\n  /** Whether to auto-fetch names on mount (default: true) */\n  autoFetch?: boolean\n  /** Callback on successful register or deregister */\n  onSuccess?: (result: { txid?: string; error?: string }) => void\n  /** Callback on error */\n  onError?: (error: Error) => void\n  /** Number of skeleton rows to show while loading (default: 3) */\n  skeletonCount?: number\n  /** Optional CSS class name */\n  className?: string\n}\n\n// ---------------------------------------------------------------------------\n// Composed component\n// ---------------------------------------------------------------------------\n\n/**\n * Full OpNS name manager block. Lists owned OpNS names from the connected\n * wallet and provides register/deregister identity binding actions.\n *\n * Must be rendered inside a `WalletProvider` from `@1sat/react`.\n *\n * @example\n * ```tsx\n * import { WalletProvider } from \"@1sat/react\"\n * import { OpnsManager } from \"@/components/blocks/opns-manager\"\n *\n * function App() {\n *   return (\n *     <WalletProvider>\n *       <OpnsManager\n *         onSuccess={(r) => console.log(\"txid:\", r.txid)}\n *         onError={(e) => console.error(e)}\n *       />\n *     </WalletProvider>\n *   )\n * }\n * ```\n */\nexport function OpnsManager({\n  autoFetch = true,\n  onSuccess,\n  onError,\n  skeletonCount,\n  className,\n}: OpnsManagerProps) {\n  const {\n    names,\n    isLoading,\n    isOperating,\n    error,\n    register,\n    deregister,\n    refresh,\n  } = useOpnsManager({ autoFetch, onSuccess, onError })\n\n  return (\n    <OpnsManagerUI\n      names={names}\n      isLoading={isLoading}\n      isOperating={isOperating}\n      error={error}\n      onRegister={register}\n      onDeregister={deregister}\n      onRefresh={refresh}\n      skeletonCount={skeletonCount}\n      className={className}\n    />\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/opns-manager/index.tsx"
    },
    {
      "path": "registry/new-york/blocks/opns-manager/opns-manager-ui.tsx",
      "content": "\"use client\"\n\nimport { useCallback, useState } from \"react\"\nimport { Globe, RefreshCw, Loader2 } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { Separator } from \"@/components/ui/separator\"\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** An OpNS name to display */\nexport interface OpnsNameDisplay {\n  /** Outpoint of the OpNS ordinal (txid_vout) */\n  outpoint: string\n  /** The human-readable name string */\n  name: string\n  /** Whether an identity key is currently bound */\n  registered: boolean\n  /** The bound identity public key, if registered */\n  identityKey?: string\n}\n\n/** Result returned by register/deregister callbacks */\nexport interface OpnsOperationResult {\n  /** Transaction ID on success */\n  txid?: string\n  /** Error message on failure */\n  error?: string\n}\n\nexport interface OpnsManagerUIProps {\n  /** List of OpNS names to display */\n  names: OpnsNameDisplay[]\n  /** Whether the name list is loading */\n  isLoading: boolean\n  /** Whether a register/deregister operation is in progress */\n  isOperating: boolean\n  /** Error from the last fetch or operation */\n  error: Error | null\n  /** Register identity binding on a name */\n  onRegister?: (name: OpnsNameDisplay) => Promise<OpnsOperationResult>\n  /** Remove identity binding from a name */\n  onDeregister?: (name: OpnsNameDisplay) => Promise<OpnsOperationResult>\n  /** Refresh the name list */\n  onRefresh?: () => void\n  /** Number of skeleton rows to show while loading (default: 3) */\n  skeletonCount?: number\n  /** Optional CSS class name */\n  className?: string\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Truncate an outpoint for display */\nfunction truncateOutpoint(outpoint: string, maxLen = 20): string {\n  if (outpoint.length <= maxLen) return outpoint\n  return `${outpoint.slice(0, 10)}...${outpoint.slice(-8)}`\n}\n\n// ---------------------------------------------------------------------------\n// Confirmation dialog state\n// ---------------------------------------------------------------------------\n\ntype ConfirmAction = \"register\" | \"deregister\"\n\ninterface ConfirmState {\n  open: boolean\n  action: ConfirmAction\n  name: OpnsNameDisplay | null\n}\n\nconst INITIAL_CONFIRM: ConfirmState = {\n  open: false,\n  action: \"register\",\n  name: null,\n}\n\n// ---------------------------------------------------------------------------\n// Sub-components\n// ---------------------------------------------------------------------------\n\ninterface NameRowProps {\n  name: OpnsNameDisplay\n  isOperating: boolean\n  onRequestRegister: (name: OpnsNameDisplay) => void\n  onRequestDeregister: (name: OpnsNameDisplay) => void\n  isLast: boolean\n}\n\nfunction NameRow({\n  name,\n  isOperating,\n  onRequestRegister,\n  onRequestDeregister,\n  isLast,\n}: NameRowProps) {\n  const handleAction = useCallback(() => {\n    if (name.registered) {\n      onRequestDeregister(name)\n    } else {\n      onRequestRegister(name)\n    }\n  }, [name, onRequestRegister, onRequestDeregister])\n\n  return (\n    <>\n      <div className=\"flex items-center gap-4 px-4 py-3\">\n        {/* Name icon */}\n        <div className=\"flex size-10 flex-shrink-0 items-center justify-center rounded-full bg-muted\">\n          <Globe\n            className=\"size-5 text-muted-foreground\"\n            aria-hidden=\"true\"\n          />\n        </div>\n\n        {/* Name + outpoint */}\n        <div className=\"flex-1 min-w-0\">\n          <div className=\"flex items-center gap-2\">\n            <span className=\"font-semibold text-sm text-foreground truncate\">\n              {name.name}\n            </span>\n            <Badge\n              variant={name.registered ? \"default\" : \"secondary\"}\n              className=\"text-[10px] px-1.5 py-0 h-5\"\n            >\n              {name.registered ? \"Registered\" : \"Available\"}\n            </Badge>\n          </div>\n          <p className=\"text-xs text-muted-foreground font-mono truncate\">\n            {truncateOutpoint(name.outpoint)}\n          </p>\n        </div>\n\n        {/* Action button */}\n        <Button\n          variant={name.registered ? \"outline\" : \"default\"}\n          size=\"sm\"\n          disabled={isOperating}\n          onClick={handleAction}\n          aria-label={\n            name.registered\n              ? `Deregister ${name.name}`\n              : `Register ${name.name}`\n          }\n        >\n          {isOperating ? (\n            <Loader2 data-icon className=\"animate-spin\" />\n          ) : null}\n          {name.registered ? \"Deregister\" : \"Register\"}\n        </Button>\n      </div>\n      {!isLast && <Separator />}\n    </>\n  )\n}\n\nfunction SkeletonRow({ isLast }: { isLast: boolean }) {\n  return (\n    <>\n      <div className=\"flex items-center gap-4 px-4 py-3\">\n        <Skeleton className=\"size-10 rounded-full flex-shrink-0\" />\n        <div className=\"flex-1 flex flex-col gap-2\">\n          <div className=\"flex items-center gap-2\">\n            <Skeleton className=\"h-4 w-24\" />\n            <Skeleton className=\"h-5 w-16 rounded-full\" />\n          </div>\n          <Skeleton className=\"h-3 w-36\" />\n        </div>\n        <Skeleton className=\"h-8 w-20 rounded-md\" />\n      </div>\n      {!isLast && <Separator />}\n    </>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// Main UI\n// ---------------------------------------------------------------------------\n\n/**\n * Pure presentation component for managing OpNS names.\n *\n * Renders a card with a list of owned OpNS names, their registration status,\n * and action buttons to register or deregister identity bindings. Includes a\n * confirmation dialog before executing on-chain operations.\n */\nexport function OpnsManagerUI({\n  names,\n  isLoading,\n  isOperating,\n  error,\n  onRegister,\n  onDeregister,\n  onRefresh,\n  skeletonCount = 3,\n  className,\n}: OpnsManagerUIProps) {\n  const [confirm, setConfirm] = useState<ConfirmState>(INITIAL_CONFIRM)\n\n  const handleRequestRegister = useCallback((name: OpnsNameDisplay) => {\n    setConfirm({ open: true, action: \"register\", name })\n  }, [])\n\n  const handleRequestDeregister = useCallback((name: OpnsNameDisplay) => {\n    setConfirm({ open: true, action: \"deregister\", name })\n  }, [])\n\n  const handleConfirm = useCallback(async () => {\n    if (!confirm.name) return\n\n    const handler =\n      confirm.action === \"register\" ? onRegister : onDeregister\n\n    if (handler) {\n      await handler(confirm.name)\n    }\n\n    setConfirm(INITIAL_CONFIRM)\n  }, [confirm, onRegister, onDeregister])\n\n  const handleCancel = useCallback(() => {\n    setConfirm(INITIAL_CONFIRM)\n  }, [])\n\n  // Loading state\n  if (isLoading && names.length === 0) {\n    return (\n      <Card className={cn(\"w-full\", className)}>\n        <CardHeader>\n          <div className=\"flex items-center justify-between\">\n            <div className=\"flex flex-col gap-1.5\">\n              <CardTitle>OpNS Names</CardTitle>\n              <CardDescription>Manage your on-chain name bindings</CardDescription>\n            </div>\n            <Skeleton className=\"size-8 rounded-md\" />\n          </div>\n        </CardHeader>\n        <CardContent className=\"p-0\">\n          {Array.from({ length: skeletonCount }, (_, i) => (\n            <SkeletonRow\n              key={`skeleton-${i}`}\n              isLast={i === skeletonCount - 1}\n            />\n          ))}\n        </CardContent>\n      </Card>\n    )\n  }\n\n  // Error state\n  if (error && names.length === 0) {\n    return (\n      <Card className={cn(\"w-full\", className)}>\n        <CardHeader>\n          <div className=\"flex items-center justify-between\">\n            <div className=\"flex flex-col gap-1.5\">\n              <CardTitle>OpNS Names</CardTitle>\n              <CardDescription>Manage your on-chain name bindings</CardDescription>\n            </div>\n            {onRefresh && (\n              <Button\n                variant=\"ghost\"\n                size=\"icon\"\n                className=\"size-8\"\n                onClick={onRefresh}\n                aria-label=\"Refresh names\"\n              >\n                <RefreshCw data-icon />\n              </Button>\n            )}\n          </div>\n        </CardHeader>\n        <CardContent className=\"flex flex-col items-center gap-2 py-10\">\n          <Globe\n            className=\"size-10 text-destructive/60\"\n            aria-hidden=\"true\"\n          />\n          <p className=\"text-sm font-medium text-destructive\">\n            Failed to load names\n          </p>\n          <p className=\"text-xs text-muted-foreground\">{error.message}</p>\n        </CardContent>\n      </Card>\n    )\n  }\n\n  // Empty state\n  if (names.length === 0) {\n    return (\n      <Card className={cn(\"w-full\", className)}>\n        <CardHeader>\n          <div className=\"flex items-center justify-between\">\n            <div className=\"flex flex-col gap-1.5\">\n              <CardTitle>OpNS Names</CardTitle>\n              <CardDescription>Manage your on-chain name bindings</CardDescription>\n            </div>\n            {onRefresh && (\n              <Button\n                variant=\"ghost\"\n                size=\"icon\"\n                className=\"size-8\"\n                onClick={onRefresh}\n                aria-label=\"Refresh names\"\n              >\n                <RefreshCw data-icon />\n              </Button>\n            )}\n          </div>\n        </CardHeader>\n        <CardContent className=\"flex flex-col items-center gap-2 py-10\">\n          <Globe\n            className=\"size-10 text-muted-foreground/50\"\n            aria-hidden=\"true\"\n          />\n          <p className=\"text-sm text-muted-foreground\">\n            No OpNS names found\n          </p>\n        </CardContent>\n      </Card>\n    )\n  }\n\n  // Name list\n  return (\n    <>\n      <Card className={cn(\"w-full overflow-hidden\", className)}>\n        <CardHeader>\n          <div className=\"flex items-center justify-between\">\n            <div className=\"flex flex-col gap-1.5\">\n              <CardTitle>OpNS Names</CardTitle>\n              <CardDescription>Manage your on-chain name bindings</CardDescription>\n            </div>\n            {onRefresh && (\n              <Button\n                variant=\"ghost\"\n                size=\"icon\"\n                className=\"size-8\"\n                onClick={onRefresh}\n                aria-label=\"Refresh names\"\n              >\n                <RefreshCw data-icon />\n              </Button>\n            )}\n          </div>\n        </CardHeader>\n        <CardContent className=\"p-0\">\n          {names.map((name, index) => (\n            <NameRow\n              key={name.outpoint}\n              name={name}\n              isOperating={isOperating}\n              onRequestRegister={handleRequestRegister}\n              onRequestDeregister={handleRequestDeregister}\n              isLast={index === names.length - 1}\n            />\n          ))}\n        </CardContent>\n      </Card>\n\n      {/* Confirmation dialog */}\n      <Dialog open={confirm.open} onOpenChange={(open) => {\n        if (!open) handleCancel()\n      }}>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>\n              {confirm.action === \"register\"\n                ? \"Register Identity\"\n                : \"Remove Identity Binding\"}\n            </DialogTitle>\n            <DialogDescription>\n              {confirm.action === \"register\"\n                ? `Bind your wallet's identity key to \"${confirm.name?.name ?? \"\"}\". This creates an on-chain transaction.`\n                : `Remove the identity binding from \"${confirm.name?.name ?? \"\"}\". This creates an on-chain transaction.`}\n            </DialogDescription>\n          </DialogHeader>\n          <DialogFooter className=\"flex gap-2 sm:gap-0\">\n            <Button\n              variant=\"outline\"\n              onClick={handleCancel}\n              disabled={isOperating}\n            >\n              Cancel\n            </Button>\n            <Button\n              variant={confirm.action === \"deregister\" ? \"destructive\" : \"default\"}\n              onClick={handleConfirm}\n              disabled={isOperating}\n            >\n              {isOperating ? (\n                <Loader2 data-icon className=\"animate-spin\" />\n              ) : null}\n              {confirm.action === \"register\" ? \"Register\" : \"Deregister\"}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </>\n  )\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/opns-manager/opns-manager-ui.tsx"
    },
    {
      "path": "registry/new-york/blocks/opns-manager/use-opns-manager.ts",
      "content": "import { useCallback, useEffect, useRef, useState } from \"react\"\nimport { useWallet } from \"@1sat/react\"\nimport {\n  getOpnsNames,\n  opnsRegister,\n  opnsDeregister,\n  createContext,\n  type OpnsOperationResponse,\n} from \"@1sat/actions\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** An OpNS name owned by the connected wallet */\nexport interface OpnsName {\n  /** Outpoint of the OpNS ordinal (txid_vout) */\n  outpoint: string\n  /** The human-readable name string */\n  name: string\n  /** Whether an identity key is currently bound */\n  registered: boolean\n  /** The bound identity public key, if registered */\n  identityKey?: string\n}\n\n/** Options for the useOpnsManager hook */\nexport interface UseOpnsManagerOptions {\n  /** Whether to auto-fetch names on mount (default: true) */\n  autoFetch?: boolean\n  /** Callback on successful register or deregister */\n  onSuccess?: (result: OpnsOperationResponse) => void\n  /** Callback on error */\n  onError?: (error: Error) => void\n}\n\n/** Return type for the useOpnsManager hook */\nexport interface UseOpnsManagerReturn {\n  /** List of OpNS names owned by the wallet */\n  names: OpnsName[]\n  /** Whether the initial name list is loading */\n  isLoading: boolean\n  /** Error from the last fetch or operation */\n  error: Error | null\n  /** Whether a register/deregister operation is in progress */\n  isOperating: boolean\n  /** Register an identity key on the given OpNS name */\n  register: (name: OpnsName) => Promise<OpnsOperationResponse>\n  /** Remove identity binding from the given OpNS name */\n  deregister: (name: OpnsName) => Promise<OpnsOperationResponse>\n  /** Refetch the list of OpNS names */\n  refresh: () => void\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Extract the name string from OpNS output tags */\nfunction extractNameFromTags(tags: string[]): string {\n  for (const tag of tags) {\n    if (tag.startsWith(\"name:\")) {\n      return tag.slice(5)\n    }\n  }\n  return \"unknown\"\n}\n\n/** Check whether the output is currently registered */\nfunction isRegistered(tags: string[]): boolean {\n  return tags.some((t) => t === \"opns:published\")\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\n/**\n * Manages OpNS names for the connected wallet. Lists owned names,\n * registers identity key bindings, and deregisters them.\n *\n * Must be rendered inside a `WalletProvider` from `@1sat/react`.\n *\n * @example\n * ```ts\n * const { names, isLoading, register, deregister, refresh } =\n *   useOpnsManager({ onSuccess: (r) => console.log(\"txid:\", r.txid) })\n * ```\n */\nexport function useOpnsManager(\n  options: UseOpnsManagerOptions = {}\n): UseOpnsManagerReturn {\n  const { autoFetch = true, onSuccess, onError } = options\n  const { wallet, status } = useWallet()\n\n  const [names, setNames] = useState<OpnsName[]>([])\n  const [isLoading, setIsLoading] = useState(false)\n  const [isOperating, setIsOperating] = useState(false)\n  const [error, setError] = useState<Error | null>(null)\n  const abortRef = useRef<AbortController | null>(null)\n\n  const fetchNames = useCallback(async () => {\n    if (!wallet || status !== \"connected\") {\n      setNames([])\n      return\n    }\n\n    abortRef.current?.abort()\n    const controller = new AbortController()\n    abortRef.current = controller\n\n    setIsLoading(true)\n    setError(null)\n\n    try {\n      const ctx = createContext(wallet)\n      const result = await getOpnsNames.execute(ctx, {})\n\n      if (controller.signal.aborted) return\n\n      const parsed: OpnsName[] = result.outputs.map((output) => {\n        const tags = output.tags ?? []\n        return {\n          outpoint: output.outpoint,\n          name: extractNameFromTags(tags),\n          registered: isRegistered(tags),\n          identityKey: undefined, // Identity key is resolved server-side\n        }\n      })\n\n      setNames(parsed)\n    } catch (err) {\n      if (err instanceof DOMException && err.name === \"AbortError\") return\n      const fetchError =\n        err instanceof Error ? err : new Error(\"Failed to fetch OpNS names\")\n      setError(fetchError)\n      onError?.(fetchError)\n      setNames([])\n    } finally {\n      setIsLoading(false)\n    }\n  }, [wallet, status, onError])\n\n  const register = useCallback(\n    async (name: OpnsName): Promise<OpnsOperationResponse> => {\n      if (!wallet) {\n        const err = new Error(\"Wallet not connected\")\n        onError?.(err)\n        return { error: err.message }\n      }\n\n      setIsOperating(true)\n      setError(null)\n\n      try {\n        const ctx = createContext(wallet)\n\n        // Resolve the full output from the wallet for this outpoint\n        const listResult = await getOpnsNames.execute(ctx, {})\n        const ordinal = listResult.outputs.find(\n          (o) => o.outpoint === name.outpoint\n        )\n\n        if (!ordinal) {\n          const err = new Error(`OpNS name \"${name.name}\" not found in wallet`)\n          setError(err)\n          onError?.(err)\n          return { error: err.message }\n        }\n\n        const result = await opnsRegister.execute(ctx, { ordinal })\n\n        if (result.error) {\n          const err = new Error(result.error)\n          setError(err)\n          onError?.(err)\n          return result\n        }\n\n        onSuccess?.(result)\n        // Refresh list after successful operation\n        void fetchNames()\n        return result\n      } catch (err) {\n        const opError =\n          err instanceof Error ? err : new Error(\"Registration failed\")\n        setError(opError)\n        onError?.(opError)\n        return { error: opError.message }\n      } finally {\n        setIsOperating(false)\n      }\n    },\n    [wallet, onSuccess, onError, fetchNames]\n  )\n\n  const deregister = useCallback(\n    async (name: OpnsName): Promise<OpnsOperationResponse> => {\n      if (!wallet) {\n        const err = new Error(\"Wallet not connected\")\n        onError?.(err)\n        return { error: err.message }\n      }\n\n      setIsOperating(true)\n      setError(null)\n\n      try {\n        const ctx = createContext(wallet)\n\n        const listResult = await getOpnsNames.execute(ctx, {})\n        const ordinal = listResult.outputs.find(\n          (o) => o.outpoint === name.outpoint\n        )\n\n        if (!ordinal) {\n          const err = new Error(`OpNS name \"${name.name}\" not found in wallet`)\n          setError(err)\n          onError?.(err)\n          return { error: err.message }\n        }\n\n        const result = await opnsDeregister.execute(ctx, { ordinal })\n\n        if (result.error) {\n          const err = new Error(result.error)\n          setError(err)\n          onError?.(err)\n          return result\n        }\n\n        onSuccess?.(result)\n        void fetchNames()\n        return result\n      } catch (err) {\n        const opError =\n          err instanceof Error ? err : new Error(\"Deregistration failed\")\n        setError(opError)\n        onError?.(opError)\n        return { error: opError.message }\n      } finally {\n        setIsOperating(false)\n      }\n    },\n    [wallet, onSuccess, onError, fetchNames]\n  )\n\n  // Auto-fetch on mount / status change\n  useEffect(() => {\n    if (autoFetch && status === \"connected\") {\n      void fetchNames()\n    }\n    return () => {\n      abortRef.current?.abort()\n    }\n  }, [autoFetch, status, fetchNames])\n\n  return {\n    names,\n    isLoading,\n    error,\n    isOperating,\n    register,\n    deregister,\n    refresh: fetchNames,\n  }\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/opns-manager/use-opns-manager.ts"
    }
  ],
  "categories": [
    "identity"
  ],
  "type": "registry:block"
}