{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tinte-editor",
  "title": "Tinte Theme Editor",
  "description": "Live theme editor with AI-powered theme generation, Browse tab for tinte.dev themes, and manual color editing. Complete shadcn/ui theme customization tool. Works entirely client-side with DOM-based CSS variable management.",
  "dependencies": [
    "@ai-sdk/openai",
    "@radix-ui/react-accordion",
    "@radix-ui/react-dialog",
    "@radix-ui/react-scroll-area",
    "@radix-ui/react-tabs",
    "@uiw/color-convert",
    "@uiw/react-color",
    "streamdown",
    "ai",
    "culori",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "input",
    "label",
    "popover",
    "dialog",
    "tabs",
    "textarea",
    "accordion",
    "@elements/tinte-logo",
    "@elements/theme-switcher-classic"
  ],
  "files": [
    {
      "path": "registry/default/blocks/tinte/tinte-editor/page.tsx",
      "content": "\"use client\";\n\nimport { ThemeSwitcherClassic } from \"@/registry/default/blocks/theme-switcher/theme-switcher-classic/components/elements/theme-switcher-classic\";\nimport { TinteEditor } from \"@/registry/default/blocks/tinte/tinte-editor/components/tinte-editor\";\n\nexport default function TinteEditorDemo() {\n  return (\n    <div className=\"min-h-screen bg-background\">\n      {/* Hero Section */}\n      <div className=\"container mx-auto px-4 py-12 sm:py-16\">\n        <div className=\"max-w-3xl mx-auto text-center space-y-6\">\n          <div className=\"space-y-3\">\n            <h1 className=\"text-3xl md:text-4xl lg:text-5xl font-bold tracking-tight\">\n              Tinte Theme Editor\n            </h1>\n            <p className=\"text-base md:text-lg text-muted-foreground max-w-2xl mx-auto\">\n              AI-powered theme generation for shadcn/ui. Click the Tinte button\n              below to start customizing.\n            </p>\n          </div>\n\n          {/* Theme Switcher */}\n          <div className=\"flex justify-center items-center gap-3 pt-2\">\n            <span className=\"text-sm text-muted-foreground\">\n              Try switching themes\n            </span>\n            <ThemeSwitcherClassic />\n          </div>\n        </div>\n      </div>\n\n      {/* Features Grid */}\n      <div className=\"container mx-auto px-4 pb-12\">\n        <div className=\"max-w-4xl mx-auto\">\n          <div className=\"grid gap-6 md:grid-cols-3\">\n            {/* Feature 1 */}\n            <div className=\"bg-card border rounded-lg p-5 space-y-2 text-center\">\n              <div className=\"w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center mx-auto\">\n                <svg\n                  className=\"w-5 h-5 text-primary\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  viewBox=\"0 0 24 24\"\n                >\n                  <title>AI Icon</title>\n                  <path\n                    strokeLinecap=\"round\"\n                    strokeLinejoin=\"round\"\n                    strokeWidth={2}\n                    d=\"M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z\"\n                  />\n                </svg>\n              </div>\n              <h3 className=\"font-semibold\">AI Generation</h3>\n              <p className=\"text-xs text-muted-foreground\">\n                Generate themes with AI\n              </p>\n            </div>\n\n            {/* Feature 2 */}\n            <div className=\"bg-card border rounded-lg p-5 space-y-2 text-center\">\n              <div className=\"w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center mx-auto\">\n                <svg\n                  className=\"w-5 h-5 text-primary\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  viewBox=\"0 0 24 24\"\n                >\n                  <title>Template Icon</title>\n                  <path\n                    strokeLinecap=\"round\"\n                    strokeLinejoin=\"round\"\n                    strokeWidth={2}\n                    d=\"M4 5a1 1 0 011-1h4a1 1 0 011 1v7a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v7a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 16a1 1 0 011-1h4a1 1 0 011 1v3a1 1 0 01-1 1H5a1 1 0 01-1-1v-3zM14 16a1 1 0 011-1h4a1 1 0 011 1v3a1 1 0 01-1 1h-4a1 1 0 01-1-1v-3z\"\n                  />\n                </svg>\n              </div>\n              <h3 className=\"font-semibold\">Browse Themes</h3>\n              <p className=\"text-xs text-muted-foreground\">\n                Explore tinte.dev community\n              </p>\n            </div>\n\n            {/* Feature 3 */}\n            <div className=\"bg-card border rounded-lg p-5 space-y-2 text-center\">\n              <div className=\"w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center mx-auto\">\n                <svg\n                  className=\"w-5 h-5 text-primary\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  viewBox=\"0 0 24 24\"\n                >\n                  <title>Edit Icon</title>\n                  <path\n                    strokeLinecap=\"round\"\n                    strokeLinejoin=\"round\"\n                    strokeWidth={2}\n                    d=\"M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z\"\n                  />\n                </svg>\n              </div>\n              <h3 className=\"font-semibold\">Manual Editor</h3>\n              <p className=\"text-xs text-muted-foreground\">\n                Edit colors & CSS directly\n              </p>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      {/* Tinte Editor - Floating Button */}\n      <TinteEditor />\n    </div>\n  );\n}\n",
      "type": "registry:page",
      "target": "app/page.tsx"
    },
    {
      "path": "registry/default/blocks/tinte/tinte-editor/components/tinte-editor.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { useChat } from \"@ai-sdk/react\";\nimport { DefaultChatTransport } from \"ai\";\nimport { formatHex, oklch } from \"culori\";\nimport { Loader2, RefreshCw, Search, X } from \"lucide-react\";\n\nimport {\n  Accordion,\n  AccordionContent,\n  AccordionItem,\n  AccordionTrigger,\n} from \"@/components/ui/accordion\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport { Textarea } from \"@/components/ui/textarea\";\n\nimport { TinteLogo } from \"@/registry/default/blocks/logos/tinte-logo/components/logos/tinte\";\nimport {\n  convertTinteToShadcn,\n  type TinteTheme,\n} from \"@/registry/default/blocks/tinte/tinte-editor/lib/tinte-to-shadcn\";\nimport { ChatInput } from \"./chat-input\";\nimport { Message as ChatMessage } from \"./chat-message\";\nimport { ColorInput } from \"./color-input\";\n\ntype ShadcnTokens = Record<string, string>;\n\ninterface ShadcnTheme {\n  light: ShadcnTokens;\n  dark: ShadcnTokens;\n}\n\ninterface TinteThemePreview {\n  id: string;\n  slug: string;\n  name: string;\n  concept?: string;\n  is_public: boolean;\n  colors: {\n    primary: string;\n    secondary: string;\n    accent: string;\n    foreground: string;\n    background: string;\n  };\n  rawTheme?: TinteTheme;\n  overrides?: {\n    shadcn?: {\n      light: ShadcnTokens;\n      dark: ShadcnTokens;\n    };\n  };\n}\n\nconst TOKEN_GROUPS = [\n  {\n    label: \"Background & Text\",\n    tokens: [\"background\", \"foreground\", \"muted\", \"muted-foreground\"],\n  },\n  {\n    label: \"Cards & Surfaces\",\n    tokens: [\"card\", \"card-foreground\", \"popover\", \"popover-foreground\"],\n  },\n  {\n    label: \"Interactive Elements\",\n    tokens: [\n      \"primary\",\n      \"primary-foreground\",\n      \"secondary\",\n      \"secondary-foreground\",\n      \"accent\",\n      \"accent-foreground\",\n    ],\n  },\n  {\n    label: \"Forms & States\",\n    tokens: [\n      \"border\",\n      \"input\",\n      \"ring\",\n      \"destructive\",\n      \"destructive-foreground\",\n    ],\n  },\n  {\n    label: \"Charts\",\n    tokens: [\"chart-1\", \"chart-2\", \"chart-3\", \"chart-4\", \"chart-5\"],\n  },\n  {\n    label: \"Sidebar\",\n    tokens: [\n      \"sidebar-background\",\n      \"sidebar-foreground\",\n      \"sidebar-primary\",\n      \"sidebar-primary-foreground\",\n      \"sidebar-accent\",\n      \"sidebar-accent-foreground\",\n      \"sidebar-border\",\n      \"sidebar-ring\",\n    ],\n  },\n] as const;\n\ninterface TinteEditorProps {\n  onChange?: (theme: ShadcnTheme) => void;\n}\n\nexport function TinteEditor({ onChange }: TinteEditorProps) {\n  const [isOpen, setIsOpen] = useState(false);\n  const [theme, setTheme] = useState<ShadcnTheme>({ light: {}, dark: {} });\n  const themeRef = useRef<ShadcnTheme>({ light: {}, dark: {} });\n  const [_originalFormats, setOriginalFormats] = useState<\n    Record<string, Record<string, string>>\n  >({\n    light: {},\n    dark: {},\n  });\n  const [mode, setMode] = useState<\"light\" | \"dark\">(\"light\");\n  const [loading, setLoading] = useState(false);\n  const [rawCss, setRawCss] = useState(\"\");\n\n  const [tinteThemes, setTinteThemes] = useState<TinteThemePreview[]>([]);\n  const [loadingTinteThemes, setLoadingTinteThemes] = useState(false);\n  const [tinteError, setTinteError] = useState<string | null>(null);\n  const [currentPage, setCurrentPage] = useState(1);\n  const [totalPages, setTotalPages] = useState(1);\n  const [hasMore, setHasMore] = useState(false);\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [activeSearch, setActiveSearch] = useState(\"\");\n  const [selectedThemeId, setSelectedThemeId] = useState<string | null>(null);\n  const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);\n\n  useEffect(() => {\n    themeRef.current = theme;\n  }, [theme]);\n\n  const [apiKeyError, setApiKeyError] = useState(false);\n  const { messages, sendMessage, status } = useChat({\n    transport: new DefaultChatTransport({\n      api: \"/api/tinte/chat\",\n    }),\n    onError: (error) => {\n      console.error(\"Chat error:\", error);\n      // Check if it's an API key error\n      if (error.message?.includes(\"OpenAI API key\")) {\n        setApiKeyError(true);\n      }\n    },\n  });\n\n  const convertToHex = useCallback((colorValue: string): string => {\n    try {\n      const trimmed = colorValue.trim();\n      if (trimmed.startsWith(\"#\")) {\n        return trimmed; // Already hex\n      }\n      const colorObj = oklch(trimmed);\n      if (colorObj) {\n        return formatHex(colorObj);\n      }\n      return colorValue;\n    } catch {\n      return colorValue;\n    }\n  }, []);\n\n  const handleApplyTheme = useCallback(\n    (newTheme: { light: ShadcnTokens; dark: ShadcnTokens }) => {\n      const lightHex: ShadcnTokens = {};\n      const darkHex: ShadcnTokens = {};\n\n      Object.entries(newTheme.light).forEach(([key, value]) => {\n        lightHex[key] = convertToHex(value);\n      });\n\n      Object.entries(newTheme.dark).forEach(([key, value]) => {\n        darkHex[key] = convertToHex(value);\n      });\n\n      const hexTheme = { light: lightHex, dark: darkHex };\n\n      setTheme(hexTheme);\n      onChange?.(hexTheme);\n\n      setOriginalFormats({\n        light: { ...lightHex },\n        dark: { ...darkHex },\n      });\n\n      setHasUnsavedChanges(true);\n\n      setTimeout(() => {\n        const styleId = \"tinte-dynamic-theme\";\n        let styleElement = document.getElementById(styleId) as HTMLStyleElement;\n\n        if (!styleElement) {\n          styleElement = document.createElement(\"style\");\n          styleElement.id = styleId;\n          document.head.appendChild(styleElement);\n        }\n\n        const lightTokens = Object.entries(hexTheme.light)\n          .map(([key, value]) => `  --${key}: ${value};`)\n          .join(\"\\n\");\n\n        const darkTokens = Object.entries(hexTheme.dark)\n          .map(([key, value]) => `  --${key}: ${value};`)\n          .join(\"\\n\");\n\n        styleElement.textContent = `:root {\\n${lightTokens}\\n}\\n\\n.dark {\\n${darkTokens}\\n}`;\n      }, 100);\n    },\n    [onChange, convertToHex],\n  );\n\n  // Detect color format\n  const detectColorFormat = useCallback(\n    (colorValue: string): \"hex\" | \"oklch\" | \"rgb\" | \"hsl\" | \"unknown\" => {\n      const trimmed = colorValue.trim();\n      if (trimmed.startsWith(\"#\")) return \"hex\";\n      if (trimmed.startsWith(\"oklch(\")) return \"oklch\";\n      if (trimmed.startsWith(\"rgb(\")) return \"rgb\";\n      if (trimmed.startsWith(\"hsl(\")) return \"hsl\";\n      return \"unknown\";\n    },\n    [],\n  );\n\n  // Load theme from DOM CSS variables\n  const loadTheme = useCallback(async () => {\n    setLoading(true);\n    try {\n      const root = document.documentElement;\n      const computedStyle = getComputedStyle(root);\n\n      // Get all CSS variable names that are theme-related\n      const allTokens = TOKEN_GROUPS.flatMap((group) => group.tokens);\n\n      const lightHex: ShadcnTokens = {};\n      const darkHex: ShadcnTokens = {};\n\n      // Read light mode variables\n      allTokens.forEach((token) => {\n        const value = computedStyle.getPropertyValue(`--${token}`).trim();\n        if (value) {\n          lightHex[token] = convertToHex(value);\n        }\n      });\n\n      // Temporarily switch to dark mode to read dark variables\n      const wasDark = root.classList.contains(\"dark\");\n      if (!wasDark) {\n        root.classList.add(\"dark\");\n      }\n\n      const darkComputedStyle = getComputedStyle(root);\n      allTokens.forEach((token) => {\n        const value = darkComputedStyle.getPropertyValue(`--${token}`).trim();\n        if (value) {\n          darkHex[token] = convertToHex(value);\n        }\n      });\n\n      // Restore original theme\n      if (!wasDark) {\n        root.classList.remove(\"dark\");\n      }\n\n      setTheme({ light: lightHex, dark: darkHex });\n      setOriginalFormats({ light: lightHex, dark: darkHex });\n    } catch (error) {\n      console.error(\"Error loading theme from DOM:\", error);\n    }\n    setLoading(false);\n  }, [convertToHex]);\n\n  // Fetch Tinte themes\n  const fetchTinteThemes = useCallback(async (page = 1, search?: string) => {\n    setLoadingTinteThemes(true);\n    setTinteError(null);\n    try {\n      const searchParam = search ? `&search=${encodeURIComponent(search)}` : \"\";\n      const response = await fetch(\n        `https://www.tinte.dev/api/themes/public?limit=20&page=${page}${searchParam}`,\n      );\n      if (!response.ok) {\n        throw new Error(\"Failed to fetch themes from Tinte\");\n      }\n      const data = await response.json();\n      setTinteThemes(data.themes || []);\n      setCurrentPage(data.pagination.page);\n      setHasMore(data.pagination.hasMore);\n      setTotalPages(Math.ceil(data.pagination.total / data.pagination.limit));\n    } catch (error) {\n      console.error(\"Error fetching Tinte themes:\", error);\n      setTinteError(\n        error instanceof Error ? error.message : \"Failed to load themes\",\n      );\n    } finally {\n      setLoadingTinteThemes(false);\n    }\n  }, []);\n\n  // Apply Tinte theme\n  const applyTinteTheme = useCallback(\n    (tinteTheme: TinteThemePreview) => {\n      let shadcnTheme: { light: ShadcnTokens; dark: ShadcnTokens } | null =\n        null;\n\n      if (tinteTheme.rawTheme) {\n        // Convert Tinte format to shadcn format\n        shadcnTheme = convertTinteToShadcn(tinteTheme.rawTheme);\n      } else if (\n        tinteTheme.overrides?.shadcn?.light &&\n        tinteTheme.overrides?.shadcn?.dark\n      ) {\n        // Use shadcn override only if it has light and dark color objects\n        shadcnTheme = tinteTheme.overrides.shadcn;\n      }\n\n      if (shadcnTheme) {\n        // Convert all colors to hex format\n        const lightHex: ShadcnTokens = {};\n        const darkHex: ShadcnTokens = {};\n\n        Object.entries(shadcnTheme.light).forEach(([key, value]) => {\n          lightHex[key] = convertToHex(value);\n        });\n\n        Object.entries(shadcnTheme.dark).forEach(([key, value]) => {\n          darkHex[key] = convertToHex(value);\n        });\n\n        const hexTheme = { light: lightHex, dark: darkHex };\n\n        setTheme(hexTheme);\n        onChange?.(hexTheme);\n        setSelectedThemeId(tinteTheme.id);\n        setHasUnsavedChanges(true);\n      }\n    },\n    [onChange, convertToHex],\n  );\n\n  // Initialize theme\n  useEffect(() => {\n    const root = document.documentElement;\n    const isDark = root.classList.contains(\"dark\");\n    setMode(isDark ? \"dark\" : \"light\");\n    loadTheme();\n  }, [loadTheme]);\n\n  // Fetch Tinte themes when dialog opens\n  useEffect(() => {\n    if (isOpen && tinteThemes.length === 0) {\n      fetchTinteThemes();\n    }\n  }, [isOpen, tinteThemes.length, fetchTinteThemes]);\n\n  const handleTokenEdit = useCallback(\n    (token: string, newValue: string) => {\n      setTheme((prev) => {\n        const updated = {\n          ...prev,\n          [mode]: {\n            ...prev[mode],\n            [token]: newValue,\n          },\n        };\n\n        onChange?.(updated);\n        return updated;\n      });\n\n      // Update original formats with new value\n      setOriginalFormats((prev) => ({\n        ...prev,\n        [mode]: {\n          ...prev[mode],\n          [token]: newValue,\n        },\n      }));\n\n      // Mark as unsaved\n      setHasUnsavedChanges(true);\n    },\n    [mode, onChange],\n  );\n\n  // Sync mode with DOM changes (controlled by next-themes)\n  useEffect(() => {\n    const observer = new MutationObserver(() => {\n      const isDark = document.documentElement.classList.contains(\"dark\");\n      setMode(isDark ? \"dark\" : \"light\");\n    });\n\n    observer.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    return () => observer.disconnect();\n  }, []);\n\n  // Generate raw CSS from theme\n  const generateRawCss = useCallback(() => {\n    if (!theme.light || !theme.dark) return \"\";\n\n    const lightTokens = Object.entries(theme.light)\n      .map(([key, value]) => `  --${key}: ${value};`)\n      .join(\"\\n\");\n\n    const darkTokens = Object.entries(theme.dark)\n      .map(([key, value]) => `  --${key}: ${value};`)\n      .join(\"\\n\");\n\n    if (!lightTokens && !darkTokens) return \"\";\n\n    return `:root {\\n${lightTokens}\\n}\\n\\n.dark {\\n${darkTokens}\\n}`;\n  }, [theme]);\n\n  // Parse raw CSS and update theme\n  const parseRawCss = useCallback(\n    (css: string) => {\n      try {\n        const light: ShadcnTokens = {};\n        const dark: ShadcnTokens = {};\n\n        // Match :root block\n        const rootMatch = css.match(/:root\\s*\\{([^}]+)\\}/);\n        if (rootMatch) {\n          const rootContent = rootMatch[1];\n          const variableMatches = rootContent.matchAll(\n            /--([^:]+):\\s*([^;]+);/g,\n          );\n          for (const match of variableMatches) {\n            const key = match[1].trim();\n            const value = match[2].trim();\n            light[key] = value;\n          }\n        }\n\n        // Match .dark block\n        const darkMatch = css.match(/\\.dark\\s*\\{([^}]+)\\}/);\n        if (darkMatch) {\n          const darkContent = darkMatch[1];\n          const variableMatches = darkContent.matchAll(\n            /--([^:]+):\\s*([^;]+);/g,\n          );\n          for (const match of variableMatches) {\n            const key = match[1].trim();\n            const value = match[2].trim();\n            dark[key] = value;\n          }\n        }\n\n        setTheme({ light, dark });\n        onChange?.({ light, dark });\n      } catch (error) {\n        console.error(\"Failed to parse CSS:\", error);\n      }\n    },\n    [onChange],\n  );\n\n  // Update raw CSS when theme changes\n  useEffect(() => {\n    setRawCss(generateRawCss());\n  }, [generateRawCss]);\n\n  // Write to globals.css file\n  const [saveStatus, setSaveStatus] = useState<\n    \"idle\" | \"saving\" | \"success\" | \"error\"\n  >(\"idle\");\n\n  const writeToGlobals = useCallback(async () => {\n    // Use ref to get the latest theme state\n    const currentTheme = themeRef.current;\n\n    if (!currentTheme.light || !currentTheme.dark) {\n      console.error(\"Theme is not fully loaded\");\n      return;\n    }\n\n    setSaveStatus(\"saving\");\n\n    try {\n      // Ensure all colors are in hex format before applying\n      const lightHex: ShadcnTokens = {};\n      const darkHex: ShadcnTokens = {};\n\n      Object.entries(currentTheme.light).forEach(([key, value]) => {\n        lightHex[key] = convertToHex(value);\n      });\n\n      Object.entries(currentTheme.dark).forEach(([key, value]) => {\n        darkHex[key] = convertToHex(value);\n      });\n\n      const styleId = \"tinte-dynamic-theme\";\n      let styleElement = document.getElementById(styleId) as HTMLStyleElement;\n\n      if (!styleElement) {\n        styleElement = document.createElement(\"style\");\n        styleElement.id = styleId;\n        document.head.appendChild(styleElement);\n      }\n\n      const lightTokens = Object.entries(lightHex)\n        .map(([key, value]) => `  --${key}: ${value};`)\n        .join(\"\\n\");\n\n      const darkTokens = Object.entries(darkHex)\n        .map(([key, value]) => `  --${key}: ${value};`)\n        .join(\"\\n\");\n\n      styleElement.textContent = `:root {\\n${lightTokens}\\n}\\n\\n.dark {\\n${darkTokens}\\n}`;\n\n      setSaveStatus(\"success\");\n      setHasUnsavedChanges(false);\n      setTimeout(() => setSaveStatus(\"idle\"), 2000);\n    } catch (error) {\n      console.error(\"Error applying theme to DOM:\", error);\n      setSaveStatus(\"error\");\n      setTimeout(() => setSaveStatus(\"idle\"), 2000);\n    }\n  }, [convertToHex]);\n\n  const _availableTokens = TOKEN_GROUPS.flatMap((group) =>\n    group.tokens.filter((token) => theme[mode]?.[token] !== undefined),\n  );\n\n  return (\n    <Dialog open={isOpen} onOpenChange={setIsOpen}>\n      {/* Floating Ball Trigger */}\n      <div className=\"fixed bottom-4 right-4 z-50\">\n        <DialogTrigger asChild>\n          <button\n            type=\"button\"\n            className=\"w-14 h-14 bg-card border-2 border-border rounded-full shadow-lg hover:scale-110 transition-all duration-200 flex items-center justify-center hover:shadow-xl\"\n            title=\"Open Theme Editor\"\n          >\n            <TinteLogo className=\"w-7 h-7 drop-shadow-sm\" />\n          </button>\n        </DialogTrigger>\n      </div>\n\n      {/* Dialog Content */}\n      <DialogContent showCloseButton={false} className=\"sm:max-w-2xl\">\n        {/* Header */}\n        <DialogHeader>\n          <div className=\"flex items-center justify-between w-full\">\n            <div className=\"flex items-center gap-2\">\n              <TinteLogo className=\"w-5 h-5\" />\n              <DialogTitle className=\"text-base\">Theme Editor</DialogTitle>\n              <a\n                href=\"https://tinte.dev\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n                className=\"text-xs text-muted-foreground hover:text-foreground transition-colors ml-1\"\n              >\n                tinte.dev ↗\n              </a>\n            </div>\n            <div className=\"flex items-center gap-2\">\n              <button\n                type=\"button\"\n                onClick={loadTheme}\n                disabled={loading}\n                className=\"p-1.5 hover:bg-accent rounded-md transition-colors disabled:opacity-50\"\n                title=\"Reload from globals.css\"\n              >\n                <RefreshCw\n                  size={14}\n                  className={loading ? \"animate-spin\" : \"\"}\n                />\n              </button>\n              <button\n                type=\"button\"\n                onClick={writeToGlobals}\n                disabled={saveStatus === \"saving\"}\n                className={`relative px-3 py-1.5 text-xs rounded-md transition-all disabled:opacity-50 disabled:cursor-not-allowed ${\n                  hasUnsavedChanges\n                    ? \"bg-primary text-primary-foreground hover:bg-primary/90 animate-pulse\"\n                    : \"bg-primary/80 text-primary-foreground hover:bg-primary/90\"\n                }`}\n              >\n                {hasUnsavedChanges && saveStatus === \"idle\" && (\n                  <span className=\"absolute -top-1 -right-1 flex h-3 w-3\">\n                    <span className=\"animate-ping absolute inline-flex h-full w-full rounded-full bg-destructive opacity-75\" />\n                    <span className=\"relative inline-flex rounded-full h-3 w-3 bg-destructive\" />\n                  </span>\n                )}\n                {saveStatus === \"saving\" && \"Saving...\"}\n                {saveStatus === \"success\" && \"✅ Saved!\"}\n                {saveStatus === \"error\" && \"❌ Error\"}\n                {saveStatus === \"idle\" &&\n                  (hasUnsavedChanges ? \"💾 Save\" : \"Save\")}\n              </button>\n            </div>\n          </div>\n        </DialogHeader>\n\n        {/* Content */}\n        <div className=\"flex-1 overflow-hidden flex flex-col\">\n          {loading ? (\n            <div className=\"flex items-center justify-center py-8\">\n              <RefreshCw className=\"animate-spin mr-2\" size={20} />\n              <span>Loading theme...</span>\n            </div>\n          ) : (\n            <Tabs\n              defaultValue=\"editor\"\n              className=\"flex-1 flex flex-col overflow-hidden\"\n            >\n              <TabsList className=\"mx-4 mt-4 mb-4\">\n                <TabsTrigger value=\"editor\">Editor</TabsTrigger>\n                <TabsTrigger value=\"browse\">Browse</TabsTrigger>\n                <TabsTrigger value=\"raw\">Raw CSS</TabsTrigger>\n                <TabsTrigger value=\"agent\">Agent</TabsTrigger>\n              </TabsList>\n\n              <TabsContent\n                value=\"editor\"\n                className=\"flex-1 h-0 flex flex-col overflow-hidden px-4 pb-4\"\n              >\n                <div className=\"h-[500px] border rounded-md bg-muted/20 overflow-y-auto p-4\">\n                  <Accordion\n                    type=\"single\"\n                    collapsible\n                    className=\"w-full space-y-2\"\n                    defaultValue=\"Background & Text\"\n                  >\n                    {TOKEN_GROUPS.map((group) => {\n                      const groupTokens = group.tokens.filter(\n                        (token) => theme[mode]?.[token] !== undefined,\n                      );\n                      if (groupTokens.length === 0) return null;\n\n                      return (\n                        <AccordionItem\n                          value={group.label}\n                          key={group.label}\n                          className=\"rounded-md border bg-background px-4 py-1 outline-none last:border-b has-focus-visible:border-ring has-focus-visible:ring-[3px] has-focus-visible:ring-ring/50\"\n                        >\n                          <AccordionTrigger className=\"py-2 text-[15px] leading-6 hover:no-underline focus-visible:ring-0\">\n                            <span className=\"uppercase tracking-wide\">\n                              {group.label} ({groupTokens.length})\n                            </span>\n                          </AccordionTrigger>\n                          <AccordionContent className=\"pb-2\">\n                            <div className=\"grid gap-3 sm:grid-cols-2\">\n                              {groupTokens.map((token) => (\n                                <div key={token} className=\"space-y-1.5\">\n                                  <div className=\"flex items-center justify-between\">\n                                    <span className=\"text-xs font-medium text-muted-foreground uppercase tracking-wide\">\n                                      {token.replace(/-/g, \" \")}\n                                    </span>\n                                    <span className=\"text-xs text-muted-foreground font-mono\">\n                                      {detectColorFormat(theme[mode][token])}\n                                    </span>\n                                  </div>\n                                  <ColorInput\n                                    value={theme[mode][token]}\n                                    onChange={(color) =>\n                                      handleTokenEdit(token, color)\n                                    }\n                                    label={token}\n                                  />\n                                </div>\n                              ))}\n                            </div>\n                          </AccordionContent>\n                        </AccordionItem>\n                      );\n                    })}\n                  </Accordion>\n                </div>\n              </TabsContent>\n\n              <TabsContent\n                value=\"browse\"\n                className=\"flex-1 h-0 flex flex-col overflow-hidden px-4 pb-4\"\n              >\n                <div className=\"flex flex-col gap-4 h-[500px]\">\n                  <div className=\"flex-1 border rounded-md bg-muted/20 overflow-y-auto p-4\">\n                    {loadingTinteThemes ? (\n                      <div className=\"flex flex-col items-center justify-center h-full gap-3\">\n                        <Loader2 className=\"animate-spin\" size={32} />\n                        <p className=\"text-sm text-muted-foreground\">\n                          Loading themes from tinte.dev...\n                        </p>\n                      </div>\n                    ) : tinteError ? (\n                      <div className=\"flex flex-col items-center justify-center h-full gap-4\">\n                        <div className=\"text-4xl\">⚠️</div>\n                        <div className=\"text-center space-y-2 max-w-md\">\n                          <h3 className=\"font-semibold text-lg\">\n                            Failed to Load Themes\n                          </h3>\n                          <p className=\"text-sm text-muted-foreground\">\n                            {tinteError}\n                          </p>\n                          <Button\n                            variant=\"outline\"\n                            onClick={() => fetchTinteThemes()}\n                            className=\"mt-2\"\n                          >\n                            <RefreshCw size={16} className=\"mr-2\" />\n                            Try Again\n                          </Button>\n                        </div>\n                      </div>\n                    ) : tinteThemes.length === 0 ? (\n                      <div className=\"flex flex-col items-center justify-center h-full gap-3\">\n                        <p className=\"text-sm text-muted-foreground\">\n                          No themes available\n                        </p>\n                        <Button\n                          variant=\"outline\"\n                          onClick={() => fetchTinteThemes()}\n                          size=\"sm\"\n                        >\n                          <RefreshCw size={16} className=\"mr-2\" />\n                          Refresh\n                        </Button>\n                      </div>\n                    ) : (\n                      <div className=\"space-y-3\">\n                        <div className=\"space-y-3 mb-4\">\n                          <div className=\"flex items-center gap-2\">\n                            <div className=\"relative flex-1\">\n                              <Input\n                                type=\"text\"\n                                placeholder=\"Search themes...\"\n                                value={searchQuery}\n                                onChange={(e) => setSearchQuery(e.target.value)}\n                                onKeyDown={(e) => {\n                                  if (e.key === \"Enter\") {\n                                    setActiveSearch(searchQuery);\n                                    fetchTinteThemes(1, searchQuery);\n                                  }\n                                }}\n                                className=\"h-9 pr-8\"\n                              />\n                              {searchQuery && (\n                                <button\n                                  type=\"button\"\n                                  onClick={() => {\n                                    setSearchQuery(\"\");\n                                    setActiveSearch(\"\");\n                                    fetchTinteThemes(1);\n                                  }}\n                                  className=\"absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-muted rounded-sm transition-colors\"\n                                >\n                                  <X className=\"h-3 w-3 text-muted-foreground\" />\n                                </button>\n                              )}\n                            </div>\n                            <Button\n                              variant=\"outline\"\n                              size=\"sm\"\n                              onClick={() => {\n                                setActiveSearch(searchQuery);\n                                fetchTinteThemes(1, searchQuery);\n                              }}\n                              disabled={!searchQuery}\n                              className=\"h-9\"\n                            >\n                              <Search className=\"h-3.5 w-3.5 mr-1.5\" />\n                              Search\n                            </Button>\n                            <Button\n                              variant=\"outline\"\n                              size=\"icon\"\n                              onClick={() =>\n                                fetchTinteThemes(currentPage, activeSearch)\n                              }\n                              title=\"Refresh themes\"\n                              className=\"h-9 w-9\"\n                            >\n                              <RefreshCw className=\"h-3.5 w-3.5\" />\n                            </Button>\n                          </div>\n                          <div className=\"flex items-center justify-between\">\n                            <p className=\"text-xs text-muted-foreground\">\n                              {tinteThemes.length} themes\n                              {activeSearch\n                                ? ` matching \"${activeSearch}\"`\n                                : \"\"}\n                            </p>\n                          </div>\n                        </div>\n                        <div className=\"grid gap-3\">\n                          {tinteThemes.map((tinteTheme) => {\n                            const isSelected =\n                              selectedThemeId === tinteTheme.id;\n                            return (\n                              <button\n                                key={tinteTheme.id}\n                                type=\"button\"\n                                onClick={() => applyTinteTheme(tinteTheme)}\n                                className={`group text-left p-4 border-2 rounded-lg transition-all relative ${\n                                  isSelected\n                                    ? \"border-primary bg-primary/10 shadow-md\"\n                                    : \"border-border hover:border-primary hover:bg-accent/50\"\n                                }`}\n                              >\n                                {isSelected && (\n                                  <div className=\"absolute top-2 right-2 bg-primary text-primary-foreground text-xs px-2 py-0.5 rounded-full font-medium\">\n                                    Selected\n                                  </div>\n                                )}\n                                <div className=\"flex items-start justify-between gap-3\">\n                                  <div className=\"flex-1 space-y-1.5\">\n                                    <h4\n                                      className={`font-medium transition-colors ${\n                                        isSelected\n                                          ? \"text-primary\"\n                                          : \"group-hover:text-primary\"\n                                      }`}\n                                    >\n                                      {tinteTheme.name}\n                                    </h4>\n                                    {tinteTheme.concept && (\n                                      <p className=\"text-xs text-muted-foreground line-clamp-2\">\n                                        {tinteTheme.concept}\n                                      </p>\n                                    )}\n                                  </div>\n                                  <div className=\"flex gap-1.5 shrink-0\">\n                                    {[\n                                      tinteTheme.colors.background,\n                                      tinteTheme.colors.primary,\n                                      tinteTheme.colors.secondary,\n                                      tinteTheme.colors.accent,\n                                      tinteTheme.colors.foreground,\n                                    ].map((color, idx) => (\n                                      <div\n                                        key={`${tinteTheme.id}-color-${idx}`}\n                                        className=\"w-6 h-6 rounded border border-border/50\"\n                                        style={{ backgroundColor: color }}\n                                        title={color}\n                                      />\n                                    ))}\n                                  </div>\n                                </div>\n                              </button>\n                            );\n                          })}\n                        </div>\n                      </div>\n                    )}\n                  </div>\n                  {/* Pagination Controls */}\n                  {!loadingTinteThemes &&\n                    !tinteError &&\n                    tinteThemes.length > 0 && (\n                      <div className=\"flex items-center justify-between px-2 py-3 border-t\">\n                        <div className=\"text-xs text-muted-foreground\">\n                          Page {currentPage} of {totalPages}\n                        </div>\n                        <div className=\"flex gap-2\">\n                          <Button\n                            variant=\"outline\"\n                            size=\"sm\"\n                            onClick={() =>\n                              fetchTinteThemes(currentPage - 1, activeSearch)\n                            }\n                            disabled={currentPage === 1 || loadingTinteThemes}\n                          >\n                            Previous\n                          </Button>\n                          <Button\n                            variant=\"outline\"\n                            size=\"sm\"\n                            onClick={() =>\n                              fetchTinteThemes(currentPage + 1, activeSearch)\n                            }\n                            disabled={!hasMore || loadingTinteThemes}\n                          >\n                            Next\n                          </Button>\n                        </div>\n                      </div>\n                    )}\n                </div>\n              </TabsContent>\n\n              <TabsContent\n                value=\"raw\"\n                className=\"flex-1 h-0 flex flex-col overflow-hidden px-4 pb-4\"\n              >\n                <Textarea\n                  value={rawCss}\n                  onChange={(e) => {\n                    setRawCss(e.target.value);\n                    parseRawCss(e.target.value);\n                  }}\n                  className=\"h-[500px] w-full bg-muted/40 font-mono text-xs resize-none border border-border focus-visible:ring-0 p-4\"\n                  placeholder=\"Paste your CSS here...\"\n                  spellCheck={false}\n                />\n              </TabsContent>\n\n              <TabsContent\n                value=\"agent\"\n                className=\"flex-1 h-0 flex flex-col overflow-hidden px-4 pb-4\"\n              >\n                <div className=\"h-[500px] flex flex-col gap-3\">\n                  <div className=\"flex-1 border rounded-md bg-muted/20 overflow-y-auto p-4 space-y-2\">\n                    {apiKeyError ? (\n                      <div className=\"flex flex-col items-center justify-center h-full gap-4\">\n                        <div className=\"text-center space-y-3 max-w-md\">\n                          <div className=\"text-4xl\">🔑</div>\n                          <h3 className=\"font-semibold text-lg\">\n                            OpenAI API Key Required\n                          </h3>\n                          <p className=\"text-muted-foreground text-sm\">\n                            To use the AI Theme Generator, you need to configure\n                            your OpenAI API key.\n                          </p>\n                          <div className=\"bg-muted rounded-lg p-4 text-left space-y-2\">\n                            <p className=\"text-xs font-medium\">\n                              Add to your{\" \"}\n                              <code className=\"bg-background px-1.5 py-0.5 rounded\">\n                                .env.local\n                              </code>{\" \"}\n                              file:\n                            </p>\n                            <pre className=\"bg-background p-2 rounded text-xs overflow-x-auto\">\n                              <code>OPENAI_API_KEY=your-api-key-here</code>\n                            </pre>\n                            <p className=\"text-xs text-muted-foreground\">\n                              Get your API key from{\" \"}\n                              <a\n                                href=\"https://platform.openai.com/api-keys\"\n                                target=\"_blank\"\n                                rel=\"noopener noreferrer\"\n                                className=\"text-primary hover:underline\"\n                              >\n                                platform.openai.com/api-keys\n                              </a>\n                            </p>\n                          </div>\n                          <Button\n                            variant=\"outline\"\n                            onClick={() => {\n                              setApiKeyError(false);\n                            }}\n                            className=\"mt-2\"\n                          >\n                            I've added the API key\n                          </Button>\n                        </div>\n                      </div>\n                    ) : messages.length === 0 ? (\n                      <div className=\"flex flex-col items-center justify-center gap-6\">\n                        <div className=\"text-center space-y-2\">\n                          <h3 className=\"font-semibold text-lg\">\n                            AI Theme Generator\n                          </h3>\n                          <p className=\"text-muted-foreground text-sm max-w-md\">\n                            Describe your ideal theme and let AI generate a\n                            complete color palette for you\n                          </p>\n                        </div>\n                        <div className=\"grid gap-2 w-full max-w-md px-4\">\n                          <p className=\"text-xs text-muted-foreground uppercase tracking-wide font-medium\">\n                            Suggested prompts:\n                          </p>\n                          <Button\n                            variant=\"outline\"\n                            onClick={() =>\n                              sendMessage({\n                                text: \"Create a purple theme with high contrast for accessibility\",\n                              })\n                            }\n                            className=\"justify-start h-auto py-3 whitespace-normal text-left\"\n                          >\n                            Create a purple theme with high contrast\n                          </Button>\n                          <Button\n                            variant=\"outline\"\n                            onClick={() =>\n                              sendMessage({\n                                text: \"Generate a warm autumn theme with orange and brown tones\",\n                              })\n                            }\n                            className=\"justify-start h-auto py-3 whitespace-normal text-left\"\n                          >\n                            Generate a warm autumn theme\n                          </Button>\n                          <Button\n                            variant=\"outline\"\n                            onClick={() =>\n                              sendMessage({\n                                text: \"Create a modern dark theme with blue accents\",\n                              })\n                            }\n                            className=\"justify-start h-auto py-3 whitespace-normal text-left\"\n                          >\n                            Create a modern dark theme with blue accents\n                          </Button>\n                          <Button\n                            variant=\"outline\"\n                            onClick={() =>\n                              sendMessage({\n                                text: \"Design a soft pastel theme perfect for a wellness app\",\n                              })\n                            }\n                            className=\"justify-start h-auto py-3 whitespace-normal text-left\"\n                          >\n                            Design a soft pastel wellness theme\n                          </Button>\n                        </div>\n                      </div>\n                    ) : (\n                      messages.map((message) => (\n                        <ChatMessage\n                          key={message.id}\n                          message={message}\n                          onApplyTheme={handleApplyTheme}\n                        />\n                      ))\n                    )}\n                  </div>\n                  <ChatInput\n                    onSubmit={(msg) => {\n                      sendMessage({ text: msg });\n                    }}\n                    disabled={status === \"submitted\" || status === \"streaming\"}\n                  />\n                </div>\n              </TabsContent>\n            </Tabs>\n          )}\n        </div>\n      </DialogContent>\n    </Dialog>\n  );\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/default/blocks/tinte/tinte-editor/components/color-input.tsx",
      "content": "\"use client\";\n\nimport type React from \"react\";\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\n\nimport { hexToHsva, hsvaToHex } from \"@uiw/color-convert\";\nimport { Colorful } from \"@uiw/react-color\";\nimport { hsl as culoriHsl, formatHex, lch, oklch, rgb } from \"culori\";\nimport { ChevronDown } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\n\ninterface ColorInputProps {\n  value: string;\n  onChange: (value: string) => void;\n  label: string;\n  disabled?: boolean;\n}\n\nconst getContrastColor = (hexColor: string): string => {\n  const hex = hexColor.replace(\"#\", \"\");\n  const r = parseInt(hex.substring(0, 2), 16);\n  const g = parseInt(hex.substring(2, 4), 16);\n  const b = parseInt(hex.substring(4, 6), 16);\n  const brightness = (r * 299 + g * 587 + b * 114) / 1000;\n  return brightness > 128 ? \"#000000\" : \"#ffffff\";\n};\n\nconst toHex = (colorValue: string): string => {\n  try {\n    if (colorValue.startsWith(\"#\")) return colorValue;\n    if (colorValue.startsWith(\"oklch(\")) {\n      const parsed = oklch(colorValue);\n      return parsed ? formatHex(parsed) : \"#000000\";\n    }\n    if (colorValue.startsWith(\"lch(\")) {\n      const parsed = lch(colorValue);\n      return parsed ? formatHex(parsed) : \"#000000\";\n    }\n    if (colorValue.startsWith(\"hsl(\")) {\n      const parsed = culoriHsl(colorValue);\n      return parsed ? formatHex(parsed) : \"#000000\";\n    }\n    if (colorValue.startsWith(\"rgb(\")) {\n      const parsed = rgb(colorValue);\n      return parsed ? formatHex(parsed) : \"#000000\";\n    }\n    return \"#000000\";\n  } catch {\n    return \"#000000\";\n  }\n};\n\nconst detectFormat = (\n  colorValue: string,\n): \"hex\" | \"rgb\" | \"hsl\" | \"oklch\" | \"lch\" => {\n  if (colorValue.startsWith(\"oklch(\")) return \"oklch\";\n  if (colorValue.startsWith(\"lch(\")) return \"lch\";\n  if (colorValue.startsWith(\"hsl(\")) return \"hsl\";\n  if (colorValue.startsWith(\"rgb(\")) return \"rgb\";\n  return \"hex\";\n};\n\nconst fromHex = (\n  hexValue: string,\n  format: \"hex\" | \"rgb\" | \"hsl\" | \"oklch\" | \"lch\",\n): string => {\n  try {\n    const rgbColor = rgb(hexValue);\n    if (!rgbColor) return hexValue;\n    switch (format) {\n      case \"hex\":\n        return hexValue;\n      case \"rgb\": {\n        const r = Math.round(rgbColor.r * 255);\n        const g = Math.round(rgbColor.g * 255);\n        const b = Math.round(rgbColor.b * 255);\n        return `rgb(${r} ${g} ${b})`;\n      }\n      case \"hsl\": {\n        const hslColor = culoriHsl(hexValue);\n        if (!hslColor) return hexValue;\n        const h = Math.round(hslColor.h || 0);\n        const s = Math.round((hslColor.s || 0) * 100);\n        const l = Math.round((hslColor.l || 0) * 100);\n        return `hsl(${h} ${s}% ${l}%)`;\n      }\n      case \"oklch\": {\n        const oklchColor = oklch(hexValue);\n        if (!oklchColor) return hexValue;\n        const l = (oklchColor.l || 0).toFixed(3);\n        const c = (oklchColor.c || 0).toFixed(3);\n        const h = Math.round(oklchColor.h || 0);\n        return `oklch(${l} ${c} ${h})`;\n      }\n      case \"lch\": {\n        const lchColor = lch(hexValue);\n        if (!lchColor) return hexValue;\n        const l = Math.round(lchColor.l || 0);\n        const c = Math.round(lchColor.c || 0);\n        const h = Math.round(lchColor.h || 0);\n        return `lch(${l} ${c} ${h})`;\n      }\n      default:\n        return hexValue;\n    }\n  } catch {\n    return hexValue;\n  }\n};\n\nexport function ColorInput({\n  value,\n  onChange,\n  label,\n  disabled = false,\n}: ColorInputProps) {\n  const [isOpen, setIsOpen] = useState(false);\n  const [inputValue, setInputValue] = useState(value);\n  const [activeTab, setActiveTab] = useState(\"hex\");\n  const originalFormat = useMemo(() => detectFormat(value), [value]);\n\n  useEffect(() => {\n    setInputValue(value);\n  }, [value]);\n\n  const hexColor = useMemo(() => toHex(inputValue), [inputValue]);\n  const contrastColor = useMemo(() => {\n    if (!/^#[0-9A-Fa-f]{6}$/.test(hexColor)) return \"#000000\";\n    return getContrastColor(hexColor);\n  }, [hexColor]);\n\n  const colorValues = useMemo(() => {\n    if (!/^#[0-9A-Fa-f]{6}$/.test(hexColor)) {\n      return {\n        hex: \"#000000\",\n        rgb: { r: 0, g: 0, b: 0 },\n        hsl: { h: 0, s: 0, l: 0 },\n        oklch: { l: 0, c: 0, h: 0 },\n        lch: { l: 0, c: 0, h: 0 },\n      };\n    }\n    const rgbColor = rgb(hexColor);\n    const hslColor = culoriHsl(hexColor);\n    const oklchColor = oklch(hexColor);\n    const lchColor = lch(hexColor);\n    return {\n      hex: hexColor,\n      rgb: rgbColor\n        ? {\n            r: Math.round(rgbColor.r * 255),\n            g: Math.round(rgbColor.g * 255),\n            b: Math.round(rgbColor.b * 255),\n          }\n        : { r: 0, g: 0, b: 0 },\n      hsl: hslColor\n        ? {\n            h: Math.round(hslColor.h || 0),\n            s: Math.round((hslColor.s || 0) * 100),\n            l: Math.round((hslColor.l || 0) * 100),\n          }\n        : { h: 0, s: 0, l: 0 },\n      oklch: oklchColor\n        ? {\n            l: Math.round((oklchColor.l || 0) * 100),\n            c: Math.round((oklchColor.c || 0) * 100),\n            h: Math.round(oklchColor.h || 0),\n          }\n        : { l: 0, c: 0, h: 0 },\n      lch: lchColor\n        ? {\n            l: Math.round(lchColor.l || 0),\n            c: Math.round(lchColor.c || 0),\n            h: Math.round(lchColor.h || 0),\n          }\n        : { l: 0, c: 0, h: 0 },\n    };\n  }, [hexColor]);\n\n  // Compute display value based on active tab\n  const displayValue = useMemo(() => {\n    switch (activeTab) {\n      case \"hex\":\n        return colorValues.hex;\n      case \"rgb\":\n        return `rgb(${colorValues.rgb.r} ${colorValues.rgb.g} ${colorValues.rgb.b})`;\n      case \"hsl\":\n        return `hsl(${colorValues.hsl.h} ${colorValues.hsl.s}% ${colorValues.hsl.l}%)`;\n      case \"oklch\":\n        return `oklch(${(colorValues.oklch.l / 100).toFixed(3)} ${(colorValues.oklch.c / 100).toFixed(3)} ${colorValues.oklch.h})`;\n      case \"lch\":\n        return `lch(${colorValues.lch.l} ${colorValues.lch.c} ${colorValues.lch.h})`;\n      default:\n        return colorValues.hex;\n    }\n  }, [activeTab, colorValues]);\n\n  const handleInputChange = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      const newValue = e.target.value;\n      setInputValue(newValue);\n      if (/^#[0-9A-Fa-f]{6}$/.test(newValue)) {\n        onChange(fromHex(newValue, originalFormat));\n      }\n    },\n    [onChange, originalFormat],\n  );\n\n  const handleInputBlur = useCallback(() => {\n    if (!/^#[0-9A-Fa-f]{6}$/.test(inputValue)) {\n      setInputValue(value);\n    }\n  }, [inputValue, value]);\n\n  const handleColorfulChange = useCallback(\n    (newColor: { hsva: { h: number; s: number; v: number; a: number } }) => {\n      const hex = hsvaToHex(newColor.hsva);\n      setInputValue(hex);\n      onChange(fromHex(hex, originalFormat));\n    },\n    [onChange, originalFormat],\n  );\n\n  const handleQuickColorClick = useCallback(\n    (quickColor: string) => {\n      setInputValue(quickColor);\n      onChange(fromHex(quickColor, originalFormat));\n    },\n    [onChange, originalFormat],\n  );\n\n  const handleColorSpaceChange = useCallback(\n    (values: Record<string, number>, colorSpace: string) => {\n      let newHex: string | undefined;\n      switch (colorSpace) {\n        case \"rgb\":\n          newHex = formatHex(\n            rgb({\n              mode: \"rgb\",\n              r: values.r / 255,\n              g: values.g / 255,\n              b: values.b / 255,\n            }),\n          );\n          break;\n        case \"hsl\":\n          newHex = formatHex(\n            culoriHsl({\n              mode: \"hsl\",\n              h: values.h,\n              s: values.s / 100,\n              l: values.l / 100,\n            }),\n          );\n          break;\n        case \"oklch\":\n          newHex = formatHex(\n            oklch({\n              mode: \"oklch\",\n              l: values.l / 100,\n              c: values.c / 100,\n              h: values.h,\n            }),\n          );\n          break;\n        case \"lch\":\n          newHex = formatHex(\n            lch({ mode: \"lch\", l: values.l, c: values.c, h: values.h }),\n          );\n          break;\n        default:\n          return;\n      }\n      if (newHex && /^#[0-9A-Fa-f]{6}$/.test(newHex)) {\n        setInputValue(newHex);\n        onChange(fromHex(newHex, originalFormat));\n      }\n    },\n    [onChange, originalFormat],\n  );\n\n  const hsvaColor = useMemo(() => {\n    if (!/^#[0-9A-Fa-f]{6}$/.test(hexColor)) {\n      return { h: 0, s: 0, v: 0, a: 1 };\n    }\n    return hexToHsva(hexColor);\n  }, [hexColor]);\n\n  const quickColors = useMemo(\n    () => [\n      \"#000000\",\n      \"#ffffff\",\n      \"#ef4444\",\n      \"#22c55e\",\n      \"#3b82f6\",\n      \"#f59e0b\",\n      \"#a855f7\",\n      \"#ec4899\",\n    ],\n    [],\n  );\n\n  return (\n    <div className=\"flex items-center gap-3 py-2\">\n      <Popover\n        open={isOpen && !disabled}\n        onOpenChange={(open) => !disabled && setIsOpen(open)}\n      >\n        <PopoverTrigger asChild>\n          <Button\n            variant=\"outline\"\n            className={cn(\n              \"h-10 justify-between font-mono text-sm w-full\",\n              disabled && \"opacity-50 cursor-not-allowed\",\n            )}\n            disabled={disabled}\n            style={{ backgroundColor: hexColor, color: contrastColor }}\n          >\n            <span className=\"truncate\">{displayValue}</span>\n            <ChevronDown className=\"ml-2 h-4 w-4 shrink-0 opacity-70\" />\n          </Button>\n        </PopoverTrigger>\n        <PopoverContent className=\"w-[320px] p-0\" align=\"end\" sideOffset={8}>\n          <div className=\"p-4 space-y-4\">\n            <Colorful\n              color={hsvaColor}\n              onChange={handleColorfulChange}\n              disableAlpha={true}\n              style={{ width: \"100%\" }}\n            />\n\n            <Tabs\n              value={activeTab}\n              onValueChange={setActiveTab}\n              className=\"w-full\"\n            >\n              <TabsList className=\"grid w-full grid-cols-5 h-9\">\n                <TabsTrigger value=\"hex\" className=\"text-xs\">\n                  HEX\n                </TabsTrigger>\n                <TabsTrigger value=\"rgb\" className=\"text-xs\">\n                  RGB\n                </TabsTrigger>\n                <TabsTrigger value=\"hsl\" className=\"text-xs\">\n                  HSL\n                </TabsTrigger>\n                <TabsTrigger value=\"oklch\" className=\"text-xs\">\n                  OKLCH\n                </TabsTrigger>\n                <TabsTrigger value=\"lch\" className=\"text-xs\">\n                  LCH\n                </TabsTrigger>\n              </TabsList>\n\n              <TabsContent value=\"hex\" className=\"mt-3 space-y-0\">\n                <Input\n                  value={inputValue}\n                  onChange={handleInputChange}\n                  onBlur={handleInputBlur}\n                  placeholder=\"#000000\"\n                  className=\"font-mono text-sm h-9\"\n                  disabled={disabled}\n                />\n              </TabsContent>\n\n              <TabsContent value=\"rgb\" className=\"mt-3 space-y-0\">\n                <div className=\"grid grid-cols-3 gap-2\">\n                  <div className=\"space-y-1.5\">\n                    <Label className=\"text-xs text-muted-foreground\">R</Label>\n                    <Input\n                      type=\"number\"\n                      value={colorValues.rgb.r}\n                      onChange={(e) =>\n                        handleColorSpaceChange(\n                          { ...colorValues.rgb, r: Number(e.target.value) },\n                          \"rgb\",\n                        )\n                      }\n                      min=\"0\"\n                      max=\"255\"\n                      className=\"h-9 text-sm\"\n                    />\n                  </div>\n                  <div className=\"space-y-1.5\">\n                    <Label className=\"text-xs text-muted-foreground\">G</Label>\n                    <Input\n                      type=\"number\"\n                      value={colorValues.rgb.g}\n                      onChange={(e) =>\n                        handleColorSpaceChange(\n                          { ...colorValues.rgb, g: Number(e.target.value) },\n                          \"rgb\",\n                        )\n                      }\n                      min=\"0\"\n                      max=\"255\"\n                      className=\"h-9 text-sm\"\n                    />\n                  </div>\n                  <div className=\"space-y-1.5\">\n                    <Label className=\"text-xs text-muted-foreground\">B</Label>\n                    <Input\n                      type=\"number\"\n                      value={colorValues.rgb.b}\n                      onChange={(e) =>\n                        handleColorSpaceChange(\n                          { ...colorValues.rgb, b: Number(e.target.value) },\n                          \"rgb\",\n                        )\n                      }\n                      min=\"0\"\n                      max=\"255\"\n                      className=\"h-9 text-sm\"\n                    />\n                  </div>\n                </div>\n              </TabsContent>\n\n              <TabsContent value=\"hsl\" className=\"mt-3 space-y-0\">\n                <div className=\"grid grid-cols-3 gap-2\">\n                  <div className=\"space-y-1.5\">\n                    <Label className=\"text-xs text-muted-foreground\">H</Label>\n                    <Input\n                      type=\"number\"\n                      value={colorValues.hsl.h}\n                      onChange={(e) =>\n                        handleColorSpaceChange(\n                          { ...colorValues.hsl, h: Number(e.target.value) },\n                          \"hsl\",\n                        )\n                      }\n                      min=\"0\"\n                      max=\"360\"\n                      className=\"h-9 text-sm\"\n                    />\n                  </div>\n                  <div className=\"space-y-1.5\">\n                    <Label className=\"text-xs text-muted-foreground\">S</Label>\n                    <Input\n                      type=\"number\"\n                      value={colorValues.hsl.s}\n                      onChange={(e) =>\n                        handleColorSpaceChange(\n                          { ...colorValues.hsl, s: Number(e.target.value) },\n                          \"hsl\",\n                        )\n                      }\n                      min=\"0\"\n                      max=\"100\"\n                      className=\"h-9 text-sm\"\n                    />\n                  </div>\n                  <div className=\"space-y-1.5\">\n                    <Label className=\"text-xs text-muted-foreground\">L</Label>\n                    <Input\n                      type=\"number\"\n                      value={colorValues.hsl.l}\n                      onChange={(e) =>\n                        handleColorSpaceChange(\n                          { ...colorValues.hsl, l: Number(e.target.value) },\n                          \"hsl\",\n                        )\n                      }\n                      min=\"0\"\n                      max=\"100\"\n                      className=\"h-9 text-sm\"\n                    />\n                  </div>\n                </div>\n              </TabsContent>\n\n              <TabsContent value=\"oklch\" className=\"mt-3 space-y-0\">\n                <div className=\"grid grid-cols-3 gap-2\">\n                  <div className=\"space-y-1.5\">\n                    <Label className=\"text-xs text-muted-foreground\">L</Label>\n                    <Input\n                      type=\"number\"\n                      value={colorValues.oklch.l}\n                      onChange={(e) =>\n                        handleColorSpaceChange(\n                          { ...colorValues.oklch, l: Number(e.target.value) },\n                          \"oklch\",\n                        )\n                      }\n                      min=\"0\"\n                      max=\"100\"\n                      step=\"0.1\"\n                      className=\"h-9 text-sm\"\n                    />\n                  </div>\n                  <div className=\"space-y-1.5\">\n                    <Label className=\"text-xs text-muted-foreground\">C</Label>\n                    <Input\n                      type=\"number\"\n                      value={colorValues.oklch.c}\n                      onChange={(e) =>\n                        handleColorSpaceChange(\n                          { ...colorValues.oklch, c: Number(e.target.value) },\n                          \"oklch\",\n                        )\n                      }\n                      min=\"0\"\n                      max=\"100\"\n                      step=\"0.1\"\n                      className=\"h-9 text-sm\"\n                    />\n                  </div>\n                  <div className=\"space-y-1.5\">\n                    <Label className=\"text-xs text-muted-foreground\">H</Label>\n                    <Input\n                      type=\"number\"\n                      value={colorValues.oklch.h}\n                      onChange={(e) =>\n                        handleColorSpaceChange(\n                          { ...colorValues.oklch, h: Number(e.target.value) },\n                          \"oklch\",\n                        )\n                      }\n                      min=\"0\"\n                      max=\"360\"\n                      className=\"h-9 text-sm\"\n                    />\n                  </div>\n                </div>\n              </TabsContent>\n\n              <TabsContent value=\"lch\" className=\"mt-3 space-y-0\">\n                <div className=\"grid grid-cols-3 gap-2\">\n                  <div className=\"space-y-1.5\">\n                    <Label className=\"text-xs text-muted-foreground\">L</Label>\n                    <Input\n                      type=\"number\"\n                      value={colorValues.lch.l}\n                      onChange={(e) =>\n                        handleColorSpaceChange(\n                          { ...colorValues.lch, l: Number(e.target.value) },\n                          \"lch\",\n                        )\n                      }\n                      min=\"0\"\n                      max=\"100\"\n                      className=\"h-9 text-sm\"\n                    />\n                  </div>\n                  <div className=\"space-y-1.5\">\n                    <Label className=\"text-xs text-muted-foreground\">C</Label>\n                    <Input\n                      type=\"number\"\n                      value={colorValues.lch.c}\n                      onChange={(e) =>\n                        handleColorSpaceChange(\n                          { ...colorValues.lch, c: Number(e.target.value) },\n                          \"lch\",\n                        )\n                      }\n                      min=\"0\"\n                      max=\"150\"\n                      className=\"h-9 text-sm\"\n                    />\n                  </div>\n                  <div className=\"space-y-1.5\">\n                    <Label className=\"text-xs text-muted-foreground\">H</Label>\n                    <Input\n                      type=\"number\"\n                      value={colorValues.lch.h}\n                      onChange={(e) =>\n                        handleColorSpaceChange(\n                          { ...colorValues.lch, h: Number(e.target.value) },\n                          \"lch\",\n                        )\n                      }\n                      min=\"0\"\n                      max=\"360\"\n                      className=\"h-9 text-sm\"\n                    />\n                  </div>\n                </div>\n              </TabsContent>\n            </Tabs>\n\n            <div className=\"space-y-2\">\n              <Label className=\"text-xs text-muted-foreground\">\n                Quick Colors\n              </Label>\n              <div className=\"grid grid-cols-8 gap-2\">\n                {quickColors.map((quickColor) => (\n                  <button\n                    key={quickColor}\n                    type=\"button\"\n                    className=\"w-full aspect-square rounded border-2 border-border hover:border-foreground hover:scale-110 transition-all\"\n                    style={{ backgroundColor: quickColor }}\n                    onClick={() => handleQuickColorClick(quickColor)}\n                    disabled={disabled}\n                    aria-label={`Quick color ${quickColor}`}\n                  />\n                ))}\n              </div>\n            </div>\n          </div>\n        </PopoverContent>\n      </Popover>\n    </div>\n  );\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/default/blocks/tinte/tinte-editor/components/chat-container.tsx",
      "content": "import type { HTMLAttributes } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type ChatContainerProps = HTMLAttributes<HTMLDivElement>;\n\nexport const ChatContainer = ({\n  className,\n  children,\n  ...props\n}: ChatContainerProps) => (\n  <div\n    className={cn(\n      \"flex-1 overflow-y-auto p-4 space-y-2 bg-muted/20 rounded-md border\",\n      className,\n    )}\n    {...props}\n  >\n    {children}\n  </div>\n);\n",
      "type": "registry:component"
    },
    {
      "path": "registry/default/blocks/tinte/tinte-editor/components/chat-input.tsx",
      "content": "import type { ComponentProps, FormEvent } from \"react\";\nimport { useState } from \"react\";\n\nimport { SendIcon } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Textarea } from \"@/components/ui/textarea\";\n\nexport type ChatInputProps = Omit<ComponentProps<\"form\">, \"onSubmit\"> & {\n  onSubmit: (message: string) => void;\n  disabled?: boolean;\n  placeholder?: string;\n};\n\nexport const ChatInput = ({\n  className,\n  onSubmit,\n  disabled,\n  placeholder = \"Ask the AI to generate or modify your theme...\",\n  ...props\n}: ChatInputProps) => {\n  const [input, setInput] = useState(\"\");\n\n  const handleSubmit = (e: FormEvent) => {\n    e.preventDefault();\n    if (!input.trim() || disabled) return;\n    onSubmit(input);\n    setInput(\"\");\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n    if (e.key === \"Enter\" && !e.shiftKey) {\n      e.preventDefault();\n      handleSubmit(e);\n    }\n  };\n\n  return (\n    <form\n      className={cn(\"flex gap-2 border rounded-md p-2\", className)}\n      onSubmit={handleSubmit}\n      {...props}\n    >\n      <Textarea\n        value={input}\n        onChange={(e) => setInput(e.target.value)}\n        onKeyDown={handleKeyDown}\n        placeholder={placeholder}\n        disabled={disabled}\n        className=\"flex-1 font-sans text-sm resize-none\"\n        style={{ minHeight: \"80px\" }}\n      />\n      <Button\n        type=\"submit\"\n        size=\"icon\"\n        disabled={!input.trim() || disabled}\n        className=\"shrink-0 self-end\"\n      >\n        <SendIcon className=\"size-4\" />\n      </Button>\n    </form>\n  );\n};\n",
      "type": "registry:component"
    },
    {
      "path": "registry/default/blocks/tinte/tinte-editor/components/chat-message.tsx",
      "content": "import type { HTMLAttributes } from \"react\";\n\nimport type { UIMessage } from \"ai\";\nimport { Streamdown } from \"streamdown\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type MessageProps = HTMLAttributes<HTMLDivElement> & {\n  message: UIMessage;\n  onApplyTheme?: (theme: { light: any; dark: any }) => void;\n};\n\nexport const Message = ({\n  className,\n  message,\n  onApplyTheme,\n  ...props\n}: MessageProps) => {\n  return (\n    <div\n      className={cn(\n        \"flex w-full gap-2 py-2\",\n        message.role === \"user\" ? \"justify-end\" : \"justify-start\",\n        className,\n      )}\n      {...props}\n    >\n      <div\n        className={cn(\n          \"flex flex-col gap-3 max-w-[85%]\",\n          message.role === \"user\" ? \"items-end\" : \"items-start\",\n        )}\n      >\n        {message.parts?.map((part, index) => {\n          // Handle text parts\n          if (part.type === \"text\") {\n            return (\n              <Streamdown className=\"not-prose\" key={index}>\n                {part.text}\n              </Streamdown>\n            );\n          }\n\n          // Handle tool-generate-theme parts\n          if (part.type === \"tool-generate-theme\") {\n            switch (part.state) {\n              case \"input-streaming\":\n              case \"input-available\": {\n                // Show live-building preview with partial data\n                const input = part.input as any;\n                const isStreaming = part.state === \"input-streaming\";\n\n                return (\n                  <div\n                    key={index}\n                    className=\"rounded-lg border border-border bg-card p-3 w-full shadow-sm\"\n                  >\n                    {/* Theme Header - Compact */}\n                    <div className=\"mb-2\">\n                      <div className=\"flex items-center gap-2\">\n                        <span className={isStreaming ? \"animate-pulse\" : \"\"}>\n                          ✨\n                        </span>\n                        <h4 className=\"font-semibold text-xs flex-1\">\n                          {input?.title || (\n                            <span className=\"text-muted-foreground italic\">\n                              Generating...\n                            </span>\n                          )}\n                        </h4>\n                        {isStreaming && (\n                          <span className=\"text-[9px] text-muted-foreground flex items-center gap-1\">\n                            <span className=\"animate-spin\">⚙️</span>\n                          </span>\n                        )}\n                      </div>\n                      {input?.concept && (\n                        <p className=\"text-[10px] text-muted-foreground mt-0.5 line-clamp-1\">\n                          {input.concept}\n                        </p>\n                      )}\n                    </div>\n\n                    {/* Ultra Compact Color Swatches */}\n                    <div className=\"grid grid-cols-2 gap-1.5 mb-2\">\n                      {/* Light Mode */}\n                      <div>\n                        <p className=\"text-[9px] font-medium mb-1 text-muted-foreground\">\n                          ☀️ Light\n                        </p>\n                        <div className=\"grid grid-cols-8 gap-0.5 p-1 rounded border bg-muted/20\">\n                          {input?.light ? (\n                            Object.entries(input.light)\n                              .filter(([key]) => key !== \"shadow\")\n                              .slice(0, 32)\n                              .map(([key, value]: [string, any]) => (\n                                <div\n                                  key={key}\n                                  className=\"h-3 w-full rounded-[2px] animate-in fade-in duration-200\"\n                                  style={{ backgroundColor: value }}\n                                  title={key}\n                                />\n                              ))\n                          ) : (\n                            <div className=\"col-span-8 h-3 flex items-center justify-center text-[8px] text-muted-foreground animate-pulse\">\n                              ...\n                            </div>\n                          )}\n                        </div>\n                      </div>\n\n                      {/* Dark Mode */}\n                      <div>\n                        <p className=\"text-[9px] font-medium mb-1 text-muted-foreground\">\n                          🌙 Dark\n                        </p>\n                        <div className=\"grid grid-cols-8 gap-0.5 p-1 rounded border bg-muted/20\">\n                          {input?.dark ? (\n                            Object.entries(input.dark)\n                              .filter(([key]) => key !== \"shadow\")\n                              .slice(0, 32)\n                              .map(([key, value]: [string, any]) => (\n                                <div\n                                  key={key}\n                                  className=\"h-3 w-full rounded-[2px] animate-in fade-in duration-200\"\n                                  style={{ backgroundColor: value }}\n                                  title={key}\n                                />\n                              ))\n                          ) : (\n                            <div className=\"col-span-8 h-3 flex items-center justify-center text-[8px] text-muted-foreground animate-pulse\">\n                              ...\n                            </div>\n                          )}\n                        </div>\n                      </div>\n                    </div>\n\n                    {/* Compact Info */}\n                    {input?.fonts && (\n                      <p className=\"text-[9px] text-muted-foreground text-center\">\n                        {input.fonts.sans} • {input.fonts.mono}\n                      </p>\n                    )}\n                  </div>\n                );\n              }\n\n              case \"output-available\": {\n                const result = part.output as any;\n\n                if (!result?.light || !result?.dark) {\n                  return null;\n                }\n\n                return (\n                  <div\n                    key={index}\n                    className=\"rounded-lg border-2 border-primary/20 bg-card p-3 w-full shadow-md\"\n                  >\n                    {/* Theme Header - Compact */}\n                    <div className=\"mb-2\">\n                      <div className=\"flex items-center gap-2\">\n                        <span>✨</span>\n                        <h4 className=\"font-semibold text-xs flex-1\">\n                          {result.title}\n                        </h4>\n                        <span className=\"px-1.5 py-0.5 text-[9px] bg-primary/10 text-primary rounded font-medium\">\n                          Ready\n                        </span>\n                      </div>\n                      <p className=\"text-[10px] text-muted-foreground mt-0.5 line-clamp-2\">\n                        {result.concept}\n                      </p>\n                    </div>\n\n                    {/* Ultra Compact Color Swatches */}\n                    <div className=\"grid grid-cols-2 gap-1.5 mb-2\">\n                      {/* Light Mode */}\n                      <div>\n                        <p className=\"text-[9px] font-medium mb-1 text-muted-foreground\">\n                          ☀️ Light\n                        </p>\n                        <div className=\"grid grid-cols-8 gap-0.5 p-1 rounded border bg-muted/20\">\n                          {Object.entries(result.light)\n                            .filter(([key]) => key !== \"shadow\")\n                            .slice(0, 32)\n                            .map(([key, value]: [string, any]) => (\n                              <div\n                                key={key}\n                                className=\"h-3 w-full rounded-[2px]\"\n                                style={{ backgroundColor: value }}\n                                title={key}\n                              />\n                            ))}\n                        </div>\n                      </div>\n\n                      {/* Dark Mode */}\n                      <div>\n                        <p className=\"text-[9px] font-medium mb-1 text-muted-foreground\">\n                          🌙 Dark\n                        </p>\n                        <div className=\"grid grid-cols-8 gap-0.5 p-1 rounded border bg-muted/20\">\n                          {Object.entries(result.dark)\n                            .filter(([key]) => key !== \"shadow\")\n                            .slice(0, 32)\n                            .map(([key, value]: [string, any]) => (\n                              <div\n                                key={key}\n                                className=\"h-3 w-full rounded-[2px]\"\n                                style={{ backgroundColor: value }}\n                                title={key}\n                              />\n                            ))}\n                        </div>\n                      </div>\n                    </div>\n\n                    {/* Compact Info + Apply Button */}\n                    <div className=\"space-y-1.5\">\n                      {result.fonts && (\n                        <p className=\"text-[9px] text-muted-foreground text-center\">\n                          {result.fonts.sans} • {result.fonts.mono} • Radius:{\" \"}\n                          {result.radius?.lg || \"0.5rem\"}\n                        </p>\n                      )}\n\n                      <button\n                        type=\"button\"\n                        onClick={() =>\n                          onApplyTheme?.({\n                            light: result.light,\n                            dark: result.dark,\n                          })\n                        }\n                        className=\"w-full bg-primary text-primary-foreground hover:bg-primary/90 rounded-md px-3 py-1.5 text-xs font-medium transition-colors\"\n                      >\n                        ✨ Apply Theme\n                      </button>\n                    </div>\n                  </div>\n                );\n              }\n\n              case \"output-error\":\n                return (\n                  <div\n                    key={index}\n                    className=\"rounded-lg border border-destructive bg-destructive/10 px-3 py-2 text-xs\"\n                  >\n                    <div className=\"flex items-center gap-2\">\n                      <span>⚠️</span>\n                      <span className=\"font-medium text-destructive\">\n                        Error generating theme\n                      </span>\n                    </div>\n                    <p className=\"text-destructive/80 text-[10px] mt-1\">\n                      {part.errorText}\n                    </p>\n                  </div>\n                );\n            }\n          }\n\n          return null;\n        })}\n      </div>\n    </div>\n  );\n};\n",
      "type": "registry:component"
    },
    {
      "path": "registry/default/blocks/tinte/tinte-editor/lib/tinte-to-shadcn.ts",
      "content": "import { formatHex, formatHsl, oklch, parse, rgb } from \"culori\";\n\n// ============================================================================\n// TINTE THEME TYPES (Flexoki-inspired continuous scale)\n// ============================================================================\n\nexport interface TinteBlock {\n  bg: string;\n  bg_2: string;\n  ui: string;\n  ui_2: string;\n  ui_3: string;\n  tx_3: string;\n  tx_2: string;\n  tx: string;\n  pr: string;\n  sc: string;\n  ac_1: string;\n  ac_2: string;\n  ac_3: string;\n}\n\nexport interface TinteTheme {\n  light: TinteBlock;\n  dark: TinteBlock;\n  fonts?: {\n    sans: string;\n    serif: string;\n    mono: string;\n  };\n  radius?: {\n    sm: string;\n    md: string;\n    lg: string;\n    xl: string;\n  };\n  shadows?: {\n    color: string;\n    opacity: string;\n    offsetX: string;\n    offsetY: string;\n    blur: string;\n    spread: string;\n  };\n}\n\n// ============================================================================\n// PALETTE GENERATOR (Tailwind 50-950 ramps from single color)\n// ============================================================================\n\ninterface PaletteColor {\n  name: string;\n  value: string;\n}\n\nfunction toOklch(color: string): { l: number; c: number; h: number } {\n  const oklchColor = oklch(color);\n  if (!oklchColor) throw new Error(\"Invalid color\");\n\n  return {\n    l: oklchColor.l || 0,\n    c: oklchColor.c || 0,\n    h: oklchColor.h || 0,\n  };\n}\n\nfunction oklchToRgb(\n  l: number,\n  c: number,\n  h: number,\n): { r: number; g: number; b: number } {\n  const rgbColor = rgb({ mode: \"oklch\", l, c, h });\n  if (!rgbColor) throw new Error(\"Invalid OKLCH values\");\n\n  return {\n    r: rgbColor.r || 0,\n    g: rgbColor.g || 0,\n    b: rgbColor.b || 0,\n  };\n}\n\nfunction rgbToHex(rgbColor: { r: number; g: number; b: number }): string {\n  return formatHex({ mode: \"rgb\", ...rgbColor });\n}\n\nfunction interpolate(start: number, end: number, factor: number): number {\n  return start + (end - start) * factor;\n}\n\nfunction findBestShadePosition(baseColor: string): number {\n  const baseOklch = toOklch(baseColor);\n  const targetLuminances = [\n    0.985, 0.967, 0.922, 0.87, 0.708, 0.556, 0.439, 0.371, 0.269, 0.205, 0.145,\n  ];\n\n  let bestIndex = 0;\n  let minDifference = Math.abs(targetLuminances[0] - baseOklch.l);\n\n  for (let i = 1; i < targetLuminances.length; i++) {\n    const difference = Math.abs(targetLuminances[i] - baseOklch.l);\n    if (difference < minDifference) {\n      minDifference = difference;\n      bestIndex = i;\n    }\n  }\n\n  return bestIndex;\n}\n\nfunction generateTailwindPalette(baseColor: string): PaletteColor[] {\n  const stops = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];\n  const baseOklch = toOklch(baseColor);\n  const basePosition = findBestShadePosition(baseColor);\n\n  const lightEndpoint = {\n    l: 0.985,\n    c: Math.max(0.002, baseOklch.c * 0.05),\n    h: baseOklch.h,\n  };\n  const darkEndpoint = {\n    l: 0.145,\n    c: Math.max(0.02, baseOklch.c * 0.6),\n    h: baseOklch.h,\n  };\n\n  const palette: PaletteColor[] = [];\n\n  for (let i = 0; i < stops.length; i++) {\n    let interpolatedColor: { l: number; c: number; h: number };\n\n    if (i === basePosition) {\n      interpolatedColor = baseOklch;\n    } else if (i < basePosition) {\n      const factor = (basePosition - i) / basePosition;\n      interpolatedColor = {\n        l: interpolate(baseOklch.l, lightEndpoint.l, factor),\n        c: interpolate(baseOklch.c, lightEndpoint.c, factor * 0.8),\n        h: baseOklch.h,\n      };\n    } else {\n      const factor = (i - basePosition) / (stops.length - 1 - basePosition);\n      interpolatedColor = {\n        l: interpolate(baseOklch.l, darkEndpoint.l, factor),\n        c: interpolate(baseOklch.c, darkEndpoint.c, factor * 0.9),\n        h: baseOklch.h,\n      };\n    }\n\n    const rgbColor = oklchToRgb(\n      interpolatedColor.l,\n      interpolatedColor.c,\n      interpolatedColor.h,\n    );\n    const hex = rgbToHex(rgbColor);\n\n    palette.push({\n      name: stops[i].toString(),\n      value: hex,\n    });\n  }\n\n  return palette;\n}\n\n// ============================================================================\n// TINTE TO SHADCN CONVERSION\n// ============================================================================\n\nconst DEFAULT_FONTS = {\n  \"font-sans\": \"Inter, system-ui, sans-serif\",\n  \"font-serif\": \"Georgia, serif\",\n  \"font-mono\": \"JetBrains Mono, monospace\",\n};\n\nconst DEFAULT_BASE = {\n  \"radius-sm\": \"0.25rem\",\n  \"radius-md\": \"0.5rem\",\n  \"radius-lg\": \"0.75rem\",\n  \"radius-xl\": \"1rem\",\n  radius: \"0.5rem\",\n};\n\nconst DEFAULT_SHADOWS = {\n  \"shadow-color\": \"#000000\",\n  \"shadow-opacity\": \"0.1\",\n  \"shadow-offset-x\": \"0px\",\n  \"shadow-offset-y\": \"2px\",\n  \"shadow-blur\": \"4px\",\n  \"shadow-spread\": \"0px\",\n};\n\ntype ThemeMode = \"light\" | \"dark\";\n\nconst ANCHORS = {\n  light: { primary: 600, border: 200, muted: 100, mutedFg: 600, accent: 300 },\n  dark: { primary: 400, border: 800, muted: 900, mutedFg: 300, accent: 700 },\n} as const;\n\nconst clamp01 = (v: number) => Math.max(0, Math.min(1, v));\n\nconst L = (hex: string) => {\n  const c = rgb(hex);\n  if (!c) return 0;\n  const lin = (x: number) =>\n    x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4;\n  return 0.2126 * lin(c.r) + 0.7152 * lin(c.g) + 0.0722 * lin(c.b);\n};\n\nconst contrast = (a: string, b: string) => {\n  const la = L(a),\n    lb = L(b);\n  const lighter = Math.max(la, lb),\n    darker = Math.min(la, lb);\n  return (lighter + 0.05) / (darker + 0.05);\n};\n\nconst bestTextFor = (bg: string) => {\n  const w = \"#ffffff\",\n    k = \"#000000\";\n  return contrast(w, bg) >= contrast(k, bg) ? w : k;\n};\n\nconst tweakL = (hex: string, dL: number) => {\n  const c = oklch(hex);\n  if (!c) return hex;\n  return formatHex({\n    mode: \"oklch\" as const,\n    l: clamp01(c.l + dL),\n    c: Math.max(0, c.c),\n    h: c.h,\n  });\n};\n\nfunction buildNeutralRamp(block: TinteBlock): string[] {\n  const seed = block.ui || block.ui_2 || block.ui_3 || block.bg || \"#808080\";\n  return generateTailwindPalette(seed).map((s) => s.value);\n}\n\nfunction buildRamp(seed?: string): string[] {\n  return generateTailwindPalette(seed || \"#64748b\").map((s) => s.value);\n}\n\nconst pick = (ramp: string[], step: number) => {\n  const idx = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950].indexOf(\n    step,\n  );\n  return ramp[Math.max(0, idx)];\n};\n\nconst surface = (bg: string, mode: ThemeMode, delta = 0.02) => {\n  return tweakL(bg, mode === \"light\" ? +delta : -delta);\n};\n\nfunction computeShadowVars(\n  tokens: Record<string, string>,\n): Record<string, string> {\n  const shadowColor = tokens[\"shadow-color\"] || \"hsl(0 0% 0%)\";\n  const opacity = parseFloat(tokens[\"shadow-opacity\"] || \"0.1\");\n  const offsetX = tokens[\"shadow-offset-x\"] || \"0px\";\n  const offsetY = tokens[\"shadow-offset-y\"] || \"2px\";\n  const blur = tokens[\"shadow-blur\"] || \"4px\";\n  const spread = tokens[\"shadow-spread\"] || \"0px\";\n\n  let hslColor = shadowColor;\n  if (shadowColor.startsWith(\"#\")) {\n    const parsed = parse(shadowColor);\n    if (parsed) {\n      const hslString = formatHsl(parsed);\n      hslColor = hslString.replace(/hsl\\(|\\)|\\s+/g, \"\").replace(/,/g, \" \");\n    } else {\n      hslColor = \"0 0% 0%\";\n    }\n  } else if (shadowColor.startsWith(\"hsl(\")) {\n    hslColor = shadowColor.replace(/hsl\\(|\\)/g, \"\").replace(/,/g, \" \");\n  }\n\n  const colorWithOpacity = (opacityMultiplier: number) =>\n    `hsl(${hslColor} / ${(opacity * opacityMultiplier).toFixed(2)})`;\n\n  const secondLayer = (fixedOffsetY: string, fixedBlur: string): string => {\n    const spread2 = `${(\n      parseFloat(spread.replace(\"px\", \"\") || \"0\") - 1\n    ).toString()}px`;\n    return `${offsetX} ${fixedOffsetY} ${fixedBlur} ${spread2} ${colorWithOpacity(\n      1.0,\n    )}`;\n  };\n\n  return {\n    \"shadow-2xs\": `${offsetX} ${offsetY} ${blur} ${spread} ${colorWithOpacity(\n      0.5,\n    )}`,\n    \"shadow-xs\": `${offsetX} ${offsetY} ${blur} ${spread} ${colorWithOpacity(\n      0.5,\n    )}`,\n    \"shadow-sm\": `${offsetX} ${offsetY} ${blur} ${spread} ${colorWithOpacity(\n      1.0,\n    )}, ${secondLayer(\"1px\", \"2px\")}`,\n    shadow: `${offsetX} ${offsetY} ${blur} ${spread} ${colorWithOpacity(\n      1.0,\n    )}, ${secondLayer(\"1px\", \"2px\")}`,\n    \"shadow-md\": `${offsetX} ${offsetY} ${blur} ${spread} ${colorWithOpacity(\n      1.0,\n    )}, ${secondLayer(\"2px\", \"4px\")}`,\n    \"shadow-lg\": `${offsetX} ${offsetY} ${blur} ${spread} ${colorWithOpacity(\n      1.0,\n    )}, ${secondLayer(\"4px\", \"6px\")}`,\n    \"shadow-xl\": `${offsetX} ${offsetY} ${blur} ${spread} ${colorWithOpacity(\n      1.0,\n    )}, ${secondLayer(\"8px\", \"10px\")}`,\n    \"shadow-2xl\": `${offsetX} ${offsetY} ${blur} ${spread} ${colorWithOpacity(\n      2.5,\n    )}`,\n  };\n}\n\nfunction mapTinteBlockToShadcn(\n  block: TinteBlock,\n  mode: ThemeMode,\n  extendedTheme?: TinteTheme,\n): Record<string, string> {\n  const bg = block.bg || (mode === \"light\" ? \"#ffffff\" : \"#0b0b0f\");\n  const fg = block.tx || bestTextFor(bg);\n\n  const neutralRamp = buildNeutralRamp(block);\n  const primaryRamp = buildRamp(block.pr);\n  const secondaryRamp = buildRamp(block.sc);\n  const accentRamp = buildRamp(block.ac_1 || block.ac_2 || block.pr);\n\n  const A = ANCHORS[mode];\n  const primary = pick(primaryRamp, A.primary);\n  const secondary = pick(secondaryRamp, mode === \"light\" ? 500 : 400);\n  const accent = pick(accentRamp, A.accent);\n  const muted = pick(neutralRamp, A.muted);\n  const border = pick(neutralRamp, A.border);\n\n  const ensureFg = (on: string) => bestTextFor(on);\n  const ring = tweakL(primary, mode === \"light\" ? +0.1 : -0.1);\n  const card = surface(bg, mode, 0.03);\n  const popover = surface(bg, mode, 0.03);\n\n  const destructiveSeed = block.ac_3 || \"#ef4444\";\n  const destructiveRamp = buildRamp(destructiveSeed);\n  const destructive = pick(destructiveRamp, mode === \"light\" ? 500 : 400);\n\n  const chart1 = pick(primaryRamp, 500);\n  const chart2 = pick(accentRamp, 500);\n  const chart3 = pick(primaryRamp, 300);\n  const chart4 = pick(accentRamp, 700);\n  const chart5 = pick(primaryRamp, 700);\n\n  const sidebar = bg;\n  const sidebarAccent = surface(bg, mode, 0.04);\n\n  const result: Record<string, string> = {\n    background: bg,\n    foreground: fg,\n    card,\n    \"card-foreground\": ensureFg(card),\n    popover,\n    \"popover-foreground\": ensureFg(popover),\n    primary,\n    \"primary-foreground\": ensureFg(primary),\n    secondary,\n    \"secondary-foreground\": ensureFg(secondary),\n    muted,\n    \"muted-foreground\": pick(neutralRamp, A.mutedFg),\n    accent,\n    \"accent-foreground\": ensureFg(accent),\n    destructive,\n    \"destructive-foreground\": ensureFg(destructive),\n    border,\n    input: tweakL(border, mode === \"light\" ? -0.1 : +0.1),\n    ring,\n    \"chart-1\": chart1,\n    \"chart-2\": chart2,\n    \"chart-3\": chart3,\n    \"chart-4\": chart4,\n    \"chart-5\": chart5,\n    sidebar,\n    \"sidebar-foreground\": ensureFg(sidebar),\n    \"sidebar-primary\": primary,\n    \"sidebar-primary-foreground\": ensureFg(primary),\n    \"sidebar-accent\": sidebarAccent,\n    \"sidebar-accent-foreground\": ensureFg(sidebarAccent),\n    \"sidebar-border\": border,\n    \"sidebar-ring\": ring,\n    ...DEFAULT_BASE,\n  };\n\n  // Add fonts if available\n  if (extendedTheme?.fonts) {\n    result[\"font-sans\"] =\n      `\"${extendedTheme.fonts.sans}\", ${DEFAULT_FONTS[\"font-sans\"]}`;\n    result[\"font-serif\"] =\n      `\"${extendedTheme.fonts.serif}\", ${DEFAULT_FONTS[\"font-serif\"]}`;\n    result[\"font-mono\"] =\n      `\"${extendedTheme.fonts.mono}\", ${DEFAULT_FONTS[\"font-mono\"]}`;\n  } else {\n    Object.assign(result, DEFAULT_FONTS);\n  }\n\n  // Add border radius if available\n  if (extendedTheme?.radius) {\n    result[\"radius-sm\"] = extendedTheme.radius.sm;\n    result[\"radius-md\"] = extendedTheme.radius.md;\n    result[\"radius-lg\"] = extendedTheme.radius.lg;\n    result[\"radius-xl\"] = extendedTheme.radius.xl;\n    result.radius = extendedTheme.radius.md;\n  }\n\n  // Add shadow properties if available\n  if (extendedTheme?.shadows) {\n    result[\"shadow-color\"] = extendedTheme.shadows.color;\n    result[\"shadow-opacity\"] = extendedTheme.shadows.opacity;\n    result[\"shadow-offset-x\"] = extendedTheme.shadows.offsetX;\n    result[\"shadow-offset-y\"] = extendedTheme.shadows.offsetY;\n    result[\"shadow-blur\"] = extendedTheme.shadows.blur;\n    result[\"shadow-spread\"] = extendedTheme.shadows.spread;\n  } else {\n    Object.assign(result, DEFAULT_SHADOWS);\n  }\n\n  // Add computed shadow variables\n  const shadowVars = computeShadowVars(result);\n  Object.assign(result, shadowVars);\n\n  return result;\n}\n\nexport function convertTinteToShadcn(tinte: TinteTheme) {\n  const lightBlock = mapTinteBlockToShadcn(tinte.light, \"light\", tinte);\n  const darkBlock = mapTinteBlockToShadcn(tinte.dark, \"dark\", tinte);\n\n  return {\n    light: lightBlock,\n    dark: darkBlock,\n  };\n}\n",
      "type": "registry:lib"
    }
  ],
  "envVars": {
    "OPENAI_API_KEY": "sk-..."
  },
  "docs": "https://www.tryelements.dev/docs/tinte",
  "type": "registry:block"
}