{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "theme-token-provider",
  "title": "Theme Token Provider",
  "author": "Satchmo <https://bigblocks.dev>",
  "description": "On-chain theme picker using @theme-token/sdk. Provider context and settings panel for selecting, applying, and persisting blockchain-inscribed ThemeTokens.",
  "dependencies": [
    "@theme-token/sdk",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "card",
    "input",
    "label",
    "badge",
    "separator"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/theme-token-provider/index.tsx",
      "content": "\"use client\"\n\nimport { createContext, useContext } from \"react\"\nimport {\n  useThemeToken,\n  type UseThemeTokenOptions,\n  type UseThemeTokenReturn,\n} from \"./use-theme-token\"\nimport {\n  ThemeTokenSettingsUi,\n  type ThemeTokenSettingsUiProps,\n} from \"./theme-token-settings-ui\"\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport { useThemeToken } from \"./use-theme-token\"\nexport type {\n  UseThemeTokenOptions,\n  UseThemeTokenReturn,\n  ThemeTokenStatus,\n} from \"./use-theme-token\"\nexport { ThemeTokenSettingsUi } from \"./theme-token-settings-ui\"\nexport type { ThemeTokenSettingsUiProps } from \"./theme-token-settings-ui\"\n\n// ---------------------------------------------------------------------------\n// Context\n// ---------------------------------------------------------------------------\n\nconst ThemeTokenContext = createContext<UseThemeTokenReturn | null>(null)\n\n/**\n * Access the ThemeToken context provided by `ThemeTokenProvider`.\n *\n * @throws If called outside of a `ThemeTokenProvider`.\n *\n * @example\n * ```tsx\n * const { origin, setOrigin, clearOrigin, isLoading } = useThemeTokenContext()\n * ```\n */\nexport function useThemeTokenContext(): UseThemeTokenReturn {\n  const ctx = useContext(ThemeTokenContext)\n  if (!ctx) {\n    throw new Error(\n      \"useThemeTokenContext must be used within a <ThemeTokenProvider>\",\n    )\n  }\n  return ctx\n}\n\n// ---------------------------------------------------------------------------\n// Provider\n// ---------------------------------------------------------------------------\n\n/** Props for the ThemeTokenProvider component */\nexport interface ThemeTokenProviderProps extends UseThemeTokenOptions {\n  /** Child elements to wrap */\n  children?: React.ReactNode\n  /** Additional CSS classes on the wrapper div */\n  className?: string\n}\n\n/**\n * Provides on-chain theme state to the component tree.\n *\n * On mount, reads the saved origin from localStorage (or uses `defaultOrigin`),\n * fetches the ThemeToken from the blockchain, and applies its CSS variables to\n * the document root. All children can access the theme state via\n * `useThemeTokenContext()`.\n *\n * @example\n * ```tsx\n * <ThemeTokenProvider defaultOrigin=\"abc123_0\">\n *   <App />\n * </ThemeTokenProvider>\n * ```\n */\nexport function ThemeTokenProvider({\n  children,\n  className,\n  defaultOrigin,\n  storageKey,\n  onThemeApplied,\n  onThemeCleared,\n  onError,\n}: ThemeTokenProviderProps) {\n  const themeToken = useThemeToken({\n    defaultOrigin,\n    storageKey,\n    onThemeApplied,\n    onThemeCleared,\n    onError,\n  })\n\n  return (\n    <ThemeTokenContext.Provider value={themeToken}>\n      <div className={className}>{children}</div>\n    </ThemeTokenContext.Provider>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// Settings (composed)\n// ---------------------------------------------------------------------------\n\n/** Props for the composed ThemeTokenSettings component */\nexport interface ThemeTokenSettingsProps {\n  /** Callback fired after a theme is applied */\n  onApply?: (origin: string) => void\n  /** Callback fired when the theme is cleared */\n  onClear?: () => void\n  /** Additional CSS classes */\n  className?: string\n}\n\n/**\n * Settings panel that reads from `ThemeTokenProvider` context.\n *\n * Must be rendered inside a `<ThemeTokenProvider>`. Wires the\n * `ThemeTokenSettingsUi` to the context's state and actions.\n *\n * @example\n * ```tsx\n * <ThemeTokenProvider>\n *   <ThemeTokenSettings />\n * </ThemeTokenProvider>\n * ```\n */\nexport function ThemeTokenSettings({\n  onApply,\n  onClear,\n  className,\n}: ThemeTokenSettingsProps) {\n  const { origin, theme, status, error, setOrigin, clearOrigin } =\n    useThemeTokenContext()\n\n  const handleApply = async (newOrigin: string) => {\n    await setOrigin(newOrigin)\n    onApply?.(newOrigin)\n  }\n\n  const handleClear = () => {\n    clearOrigin()\n    onClear?.()\n  }\n\n  return (\n    <ThemeTokenSettingsUi\n      origin={origin}\n      themeName={theme?.name ?? null}\n      status={status}\n      errorMessage={error?.message ?? null}\n      onApply={handleApply}\n      onClear={handleClear}\n      className={className}\n    />\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/theme-token-provider/index.tsx"
    },
    {
      "path": "registry/new-york/blocks/theme-token-provider/theme-token-settings-ui.tsx",
      "content": "\"use client\"\n\nimport { useCallback, useState } from \"react\"\nimport { Loader2, Palette, RotateCcw, Check, AlertCircle } from \"lucide-react\"\nimport { Button } from \"@/components/ui/button\"\nimport { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from \"@/components/ui/card\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { cn } from \"@/lib/utils\"\nimport type { ThemeTokenStatus } from \"./use-theme-token\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Color swatch entry for the preview grid */\ninterface ColorSwatch {\n  /** Display label */\n  label: string\n  /** CSS variable name (e.g. \"primary\") */\n  variable: string\n}\n\n/** Props for the settings panel UI */\nexport interface ThemeTokenSettingsUiProps {\n  /** Currently active theme origin, or null */\n  origin: string | null\n  /** Theme name from the fetched ThemeToken */\n  themeName: string | null\n  /** Current lifecycle status */\n  status: ThemeTokenStatus\n  /** Error message if status is \"error\" */\n  errorMessage: string | null\n  /** Callback to apply a theme by origin */\n  onApply: (origin: string) => void\n  /** Callback to clear the active theme */\n  onClear: () => void\n  /** Additional CSS classes */\n  className?: string\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst COLOR_SWATCHES: ColorSwatch[] = [\n  { label: \"Primary\", variable: \"primary\" },\n  { label: \"Secondary\", variable: \"secondary\" },\n  { label: \"Accent\", variable: \"accent\" },\n  { label: \"Muted\", variable: \"muted\" },\n  { label: \"Destructive\", variable: \"destructive\" },\n  { label: \"Background\", variable: \"background\" },\n  { label: \"Card\", variable: \"card\" },\n  { label: \"Border\", variable: \"border\" },\n]\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\n/**\n * Settings panel for selecting and applying on-chain themes.\n *\n * Displays an origin input, apply/reset buttons, the current theme status,\n * and a preview grid of the active color swatches read from CSS variables.\n */\nexport function ThemeTokenSettingsUi({\n  origin,\n  themeName,\n  status,\n  errorMessage,\n  onApply,\n  onClear,\n  className,\n}: ThemeTokenSettingsUiProps) {\n  const [inputValue, setInputValue] = useState(origin ?? \"\")\n\n  const handleApply = useCallback(() => {\n    const trimmed = inputValue.trim()\n    if (trimmed.length === 0) return\n    onApply(trimmed)\n  }, [inputValue, onApply])\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === \"Enter\") {\n        handleApply()\n      }\n    },\n    [handleApply],\n  )\n\n  const isLoading = status === \"loading\"\n  const isApplied = status === \"applied\"\n  const isError = status === \"error\"\n\n  return (\n    <Card className={cn(\"w-full max-w-md\", className)}>\n      <CardHeader>\n        <div className=\"flex items-center gap-2\">\n          <Palette className=\"size-5 text-muted-foreground\" aria-hidden=\"true\" />\n          <CardTitle>Theme Token</CardTitle>\n        </div>\n        <CardDescription>\n          Apply an on-chain theme by entering its origin (txid_vout format).\n        </CardDescription>\n      </CardHeader>\n\n      <CardContent className=\"flex flex-col gap-4\">\n        {/* Origin input */}\n        <div className=\"flex flex-col gap-2\">\n          <Label htmlFor=\"theme-origin\">Origin</Label>\n          <Input\n            id=\"theme-origin\"\n            placeholder=\"e.g. abc123def456…_0\"\n            value={inputValue}\n            onChange={(e) => setInputValue(e.target.value)}\n            onKeyDown={handleKeyDown}\n            disabled={isLoading}\n            aria-describedby=\"theme-origin-hint\"\n          />\n          <p id=\"theme-origin-hint\" className=\"text-xs text-muted-foreground\">\n            The origin outpoint of an inscribed ThemeToken.\n          </p>\n        </div>\n\n        {/* Status badge */}\n        {status !== \"idle\" && (\n          <div className=\"flex items-center gap-2\">\n            {isLoading && (\n              <Badge variant=\"secondary\" className=\"gap-1.5\">\n                <Loader2\n                  className=\"size-3 animate-spin\"\n                  aria-hidden=\"true\"\n                  data-icon=\"inline-start\"\n                />\n                Loading theme...\n              </Badge>\n            )}\n            {isApplied && (\n              <Badge variant=\"default\" className=\"gap-1.5\">\n                <Check\n                  className=\"size-3\"\n                  aria-hidden=\"true\"\n                  data-icon=\"inline-start\"\n                />\n                {themeName ?? \"Theme applied\"}\n              </Badge>\n            )}\n            {isError && (\n              <Badge variant=\"destructive\" className=\"gap-1.5\">\n                <AlertCircle\n                  className=\"size-3\"\n                  aria-hidden=\"true\"\n                  data-icon=\"inline-start\"\n                />\n                {errorMessage ?? \"Error\"}\n              </Badge>\n            )}\n          </div>\n        )}\n\n        {/* Color swatches preview */}\n        {isApplied && (\n          <>\n            <Separator />\n            <div className=\"flex flex-col gap-2\">\n              <p className=\"text-sm font-medium text-foreground\">\n                Active Colors\n              </p>\n              <div className=\"grid grid-cols-4 gap-2\">\n                {COLOR_SWATCHES.map((swatch) => (\n                  <div\n                    key={swatch.variable}\n                    className=\"flex flex-col items-center gap-1\"\n                  >\n                    <div\n                      className=\"size-8 rounded-md border border-border\"\n                      style={{\n                        backgroundColor: `var(--${swatch.variable})`,\n                      }}\n                      aria-label={`${swatch.label} color swatch`}\n                    />\n                    <span className=\"text-[10px] leading-tight text-muted-foreground\">\n                      {swatch.label}\n                    </span>\n                  </div>\n                ))}\n              </div>\n            </div>\n          </>\n        )}\n      </CardContent>\n\n      <CardFooter className=\"flex gap-2\">\n        <Button\n          onClick={handleApply}\n          disabled={isLoading || inputValue.trim().length === 0}\n          aria-busy={isLoading}\n          className=\"gap-2\"\n        >\n          {isLoading ? (\n            <Loader2\n              className=\"animate-spin\"\n              aria-hidden=\"true\"\n              data-icon=\"inline-start\"\n            />\n          ) : (\n            <Palette aria-hidden=\"true\" data-icon=\"inline-start\" />\n          )}\n          Apply Theme\n        </Button>\n\n        <Button\n          variant=\"outline\"\n          onClick={onClear}\n          disabled={isLoading || status === \"idle\"}\n          className=\"gap-2\"\n        >\n          <RotateCcw aria-hidden=\"true\" data-icon=\"inline-start\" />\n          Reset to Default\n        </Button>\n      </CardFooter>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/theme-token-provider/theme-token-settings-ui.tsx"
    },
    {
      "path": "registry/new-york/blocks/theme-token-provider/use-theme-token.ts",
      "content": "\"use client\"\n\nimport { useCallback, useEffect, useRef, useState } from \"react\"\nimport {\n  fetchThemeByOrigin,\n  applyThemeModeWithAssets,\n  clearTheme,\n  type ThemeToken,\n  type PublishedTheme,\n} from \"@theme-token/sdk\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Status of the theme fetching/applying lifecycle */\nexport type ThemeTokenStatus = \"idle\" | \"loading\" | \"applied\" | \"error\"\n\n/** Options for the useThemeToken hook */\nexport interface UseThemeTokenOptions {\n  /** Default origin to load on mount (overridden by localStorage if set) */\n  defaultOrigin?: string\n  /** localStorage key for persisting the active origin */\n  storageKey?: string\n  /** Callback fired after a theme is successfully applied */\n  onThemeApplied?: (origin: string) => void\n  /** Callback fired when the theme is cleared */\n  onThemeCleared?: () => void\n  /** Callback fired when an error occurs */\n  onError?: (error: Error) => void\n}\n\n/** Return value of the useThemeToken hook */\nexport interface UseThemeTokenReturn {\n  /** Currently active theme origin, or null if none */\n  origin: string | null\n  /** The fetched ThemeToken data, or null */\n  theme: ThemeToken | null\n  /** Current lifecycle status */\n  status: ThemeTokenStatus\n  /** Error if status is \"error\" */\n  error: Error | null\n  /** Apply a theme by origin (fetches from chain, applies CSS vars, persists) */\n  setOrigin: (origin: string) => Promise<void>\n  /** Clear the active theme and revert to default CSS vars */\n  clearOrigin: () => void\n  /** Whether a theme operation is in progress */\n  isLoading: boolean\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_STORAGE_KEY = \"bigblocks-theme-origin\"\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\n/**\n * Manages on-chain theme loading, application, and localStorage persistence.\n *\n * Reads the saved origin from localStorage on mount, fetches the ThemeToken\n * from the blockchain via `@theme-token/sdk`, and applies its CSS variables\n * to the document root. Provides imperative methods to switch or clear themes.\n */\nexport function useThemeToken({\n  defaultOrigin,\n  storageKey = DEFAULT_STORAGE_KEY,\n  onThemeApplied,\n  onThemeCleared,\n  onError,\n}: UseThemeTokenOptions = {}): UseThemeTokenReturn {\n  const [origin, setOriginState] = useState<string | null>(null)\n  const [theme, setTheme] = useState<ThemeToken | null>(null)\n  const [status, setStatus] = useState<ThemeTokenStatus>(\"idle\")\n  const [error, setError] = useState<Error | null>(null)\n\n  // Stable refs for callbacks to avoid re-triggering effects\n  const onThemeAppliedRef = useRef(onThemeApplied)\n  onThemeAppliedRef.current = onThemeApplied\n  const onThemeClearedRef = useRef(onThemeCleared)\n  onThemeClearedRef.current = onThemeCleared\n  const onErrorRef = useRef(onError)\n  onErrorRef.current = onError\n\n  // ------------------------------------------------------------------\n  // Core: fetch + apply a theme by origin\n  // ------------------------------------------------------------------\n\n  const applyOrigin = useCallback(\n    async (targetOrigin: string) => {\n      setStatus(\"loading\")\n      setError(null)\n\n      try {\n        const published: PublishedTheme | null =\n          await fetchThemeByOrigin(targetOrigin)\n\n        if (!published) {\n          throw new Error(\n            `Theme not found for origin \"${targetOrigin}\". Verify the origin is a valid ThemeToken inscription.`,\n          )\n        }\n\n        // Detect current color scheme\n        const isDark =\n          typeof document !== \"undefined\" &&\n          document.documentElement.classList.contains(\"dark\")\n        const mode = isDark ? \"dark\" : \"light\"\n\n        await applyThemeModeWithAssets(published.theme, mode)\n\n        setOriginState(targetOrigin)\n        setTheme(published.theme)\n        setStatus(\"applied\")\n\n        // Persist\n        if (typeof window !== \"undefined\") {\n          localStorage.setItem(storageKey, targetOrigin)\n        }\n\n        onThemeAppliedRef.current?.(targetOrigin)\n      } catch (err) {\n        const e = err instanceof Error ? err : new Error(String(err))\n        setError(e)\n        setStatus(\"error\")\n        onErrorRef.current?.(e)\n      }\n    },\n    [storageKey],\n  )\n\n  // ------------------------------------------------------------------\n  // Public: set a new origin\n  // ------------------------------------------------------------------\n\n  const setOrigin = useCallback(\n    async (newOrigin: string) => {\n      await applyOrigin(newOrigin)\n    },\n    [applyOrigin],\n  )\n\n  // ------------------------------------------------------------------\n  // Public: clear theme\n  // ------------------------------------------------------------------\n\n  const clearOrigin = useCallback(() => {\n    clearTheme()\n    setOriginState(null)\n    setTheme(null)\n    setStatus(\"idle\")\n    setError(null)\n\n    if (typeof window !== \"undefined\") {\n      localStorage.removeItem(storageKey)\n    }\n\n    onThemeClearedRef.current?.()\n  }, [storageKey])\n\n  // ------------------------------------------------------------------\n  // Mount: restore persisted origin (or use default)\n  // ------------------------------------------------------------------\n\n  const mountedRef = useRef(false)\n\n  useEffect(() => {\n    if (mountedRef.current) return\n    mountedRef.current = true\n\n    if (typeof window === \"undefined\") return\n\n    const saved = localStorage.getItem(storageKey)\n    const initial = saved ?? defaultOrigin\n\n    if (initial) {\n      void applyOrigin(initial)\n    }\n  }, [storageKey, defaultOrigin, applyOrigin])\n\n  return {\n    origin,\n    theme,\n    status,\n    error,\n    setOrigin,\n    clearOrigin,\n    isLoading: status === \"loading\",\n  }\n}\n",
      "type": "registry:hook",
      "target": "~/components/blocks/theme-token-provider/use-theme-token.ts"
    }
  ],
  "type": "registry:block"
}