{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "component-preview",
  "title": "Component Preview",
  "description": "A preview shell for component demos with switchable default and glass variants.",
  "dependencies": [
    "lucide-react",
    "motion",
    "shiki"
  ],
  "registryDependencies": [
    "@glasscn/utils",
    "@glasscn/button",
    "@glasscn/card",
    "@glasscn/separator",
    "@glasscn/glass-button",
    "@glasscn/glass-card",
    "@glasscn/glass-variants"
  ],
  "files": [
    {
      "path": "components/component-preview-tabs.tsx",
      "content": "\"use client\";\n\nimport { ChevronUpIcon } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { CopyButton } from \"@/components/custom/copy-button\";\nimport { Button } from \"@/components/ui/button\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { cn } from \"@/lib/utils\";\n\nimport { Card } from \"./ui/card\";\n\nexport type ComponentPreviewTabsProps = React.ComponentProps<\"div\"> & {\n  previewClassName?: string;\n  align?: \"center\" | \"start\" | \"end\";\n  hideCode?: boolean;\n  component: React.ReactNode;\n  code: string;\n  highlightedCode: string;\n  previewHighlightedCode: string;\n};\n\nexport function ComponentPreviewTabs({\n  className,\n  previewClassName,\n  align = \"center\",\n  hideCode = false,\n  component,\n  code,\n  highlightedCode,\n  previewHighlightedCode,\n}: ComponentPreviewTabsProps) {\n  const [codeVisible, setCodeVisible] = React.useState(false);\n\n  return (\n    <Card data-slot=\"component-preview\" className={cn(\"mt-4 mb-12 gap-0 overflow-hidden py-0\", className)}>\n      <div data-slot=\"preview\" dir=\"ltr\">\n        <div\n          data-align={align}\n          className={cn(\n            \"preview relative flex min-h-72 w-full justify-center p-10 data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start\",\n            previewClassName,\n          )}\n        >\n          {component}\n        </div>\n      </div>\n      {!hideCode && (\n        <>\n          <Separator />\n          <div data-slot=\"code\" data-code-visible={codeVisible} className=\"group/code bg-background relative\">\n            <div className=\"absolute top-4 right-4 z-10 flex items-center gap-2\">\n              {codeVisible && (\n                <Button\n                  type=\"button\"\n                  size=\"icon\"\n                  variant=\"outline\"\n                  className=\"text-foreground/80 hover:text-foreground h-7 w-7\"\n                  onClick={() => setCodeVisible(false)}\n                >\n                  <ChevronUpIcon className=\"h-3.5 w-3.5\" />\n                  <span className=\"sr-only\">Collapse code</span>\n                </Button>\n              )}\n              <CopyButton\n                text={code}\n                size=\"icon\"\n                variant=\"outline\"\n                className={cn(\n                  \"h-7 w-7 text-foreground/80 hover:text-foreground [&_svg]:h-3.5 [&_svg]:w-3.5\",\n                  !codeVisible && \"bg-background/50\",\n                )}\n              />\n            </div>\n            {codeVisible ? (\n              <div className=\"[&_pre]:max-h-96\" dangerouslySetInnerHTML={{ __html: highlightedCode }} />\n            ) : (\n              <div className=\"relative\">\n                <div\n                  className=\"max-h-28 overflow-hidden [&_pre]:overflow-hidden\"\n                  dangerouslySetInnerHTML={{ __html: previewHighlightedCode }}\n                />\n                <div className=\"from-background via-background/70 dark:via-background/80 absolute inset-0 flex items-end justify-center bg-linear-to-t to-transparent pb-4\">\n                  <Button type=\"button\" size=\"sm\" variant=\"outline\" onClick={() => setCodeVisible(true)}>\n                    View Code\n                  </Button>\n                </div>\n              </div>\n            )}\n          </div>\n        </>\n      )}\n    </Card>\n  );\n}\n",
      "type": "registry:component"
    },
    {
      "path": "components/glass-component-preview-tabs.tsx",
      "content": "\"use client\";\n\nimport { ChevronUpIcon } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { CopyButton } from \"@/components/custom/copy-button\";\nimport { Card } from \"@/components/ui/card\";\nimport { GlassButton } from \"@/components/ui/glasscn/glass-button\";\nimport { GlassCard } from \"@/components/ui/glasscn/glass-card\";\nimport type { FrostGlassVariant } from \"@/lib/glass-variants\";\nimport { cn } from \"@/lib/utils\";\n\nexport type GlassComponentPreviewTabsProps = React.ComponentProps<\"div\"> & {\n  previewClassName?: string;\n  align?: \"center\" | \"start\" | \"end\";\n  hideCode?: boolean;\n  component: React.ReactNode;\n  code: string;\n  highlightedCode: string;\n  previewHighlightedCode: string;\n  previewGlassVariant?: FrostGlassVariant;\n  codeGlassVariant?: FrostGlassVariant;\n};\n\nexport function GlassComponentPreviewTabs({\n  className,\n  previewClassName,\n  align = \"center\",\n  hideCode = false,\n  component,\n  code,\n  highlightedCode,\n  previewHighlightedCode,\n  codeGlassVariant = \"frosted\",\n}: GlassComponentPreviewTabsProps) {\n  const [codeVisible, setCodeVisible] = React.useState(false);\n\n  const codePanel = (\n    <>\n      <div className=\"absolute top-4 right-4 z-10 flex items-center gap-2\">\n        {codeVisible && (\n          <GlassButton\n            type=\"button\"\n            size=\"icon\"\n            variant=\"outline\"\n            glassVariant=\"liquid-refract\"\n            className=\"text-foreground/80 hover:text-foreground h-7 w-7\"\n            onClick={() => setCodeVisible(false)}\n          >\n            <ChevronUpIcon className=\"h-3.5 w-3.5\" />\n            <span className=\"sr-only\">Collapse code</span>\n          </GlassButton>\n        )}\n        <CopyButton\n          text={code}\n          size=\"icon\"\n          variant=\"outline\"\n          className={cn(\n            \"h-7 w-7 text-foreground/80 hover:text-foreground [&_svg]:h-3.5 [&_svg]:w-3.5\",\n            !codeVisible && \"bg-background/40\",\n          )}\n        />\n      </div>\n\n      {codeVisible ? (\n        <div className=\"[&_pre]:max-h-96\" dangerouslySetInnerHTML={{ __html: highlightedCode }} />\n      ) : (\n        <div className=\"relative\">\n          <div\n            className=\"max-h-28 overflow-hidden [&_pre]:overflow-hidden\"\n            dangerouslySetInnerHTML={{ __html: previewHighlightedCode }}\n          />\n          <div className=\"absolute inset-0 flex items-end justify-center bg-linear-to-t from-white/55 via-white/25 to-transparent pb-4 dark:from-black/70 dark:via-black/35\">\n            <GlassButton\n              type=\"button\"\n              size=\"sm\"\n              variant=\"outline\"\n              glassVariant=\"liquid-refract\"\n              onClick={() => setCodeVisible(true)}\n            >\n              View Code\n            </GlassButton>\n          </div>\n        </div>\n      )}\n    </>\n  );\n\n  return (\n    <div className={cn(\"mt-4 mb-12 flex flex-col gap-0\", className)}>\n      <Card\n        data-slot=\"glass-component-preview\"\n        variant=\"outline\"\n        className=\"gap-0 overflow-hidden rounded-t-[28px] rounded-b-none bg-transparent py-0\"\n      >\n        <div data-slot=\"preview\" dir=\"ltr\">\n          <div\n            data-align={align}\n            className={cn(\n              \"preview relative flex min-h-72 w-full justify-center p-10 data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start\",\n              previewClassName,\n            )}\n          >\n            {component}\n          </div>\n        </div>\n      </Card>\n\n      {!hideCode && (\n        <GlassCard\n          data-slot=\"glass-component-code\"\n          glassVariant={codeGlassVariant}\n          className=\"group/code relative -mt-px gap-0 overflow-hidden rounded-t-none rounded-b-[28px] py-0\"\n        >\n          {codePanel}\n        </GlassCard>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:component"
    },
    {
      "path": "components/custom/copy-button.tsx",
      "content": "\"use client\";\n\nimport { CheckIcon, CircleXIcon, CopyIcon } from \"lucide-react\";\nimport type { HTMLMotionProps, Variants } from \"motion/react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport type { ComponentProps } from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport type { CopyState } from \"@/hooks/use-copy-to-clipboard\";\nimport { useCopyToClipboard } from \"@/hooks/use-copy-to-clipboard\";\n\nexport const motionIconVariants: Variants = {\n  initial: { opacity: 0, scale: 0.8, filter: \"blur(2px)\" },\n  animate: { opacity: 1, scale: 1, filter: \"blur(0px)\" },\n  exit: { opacity: 0, scale: 0.8 },\n};\n\nexport const motionIconProps: HTMLMotionProps<\"span\"> = {\n  variants: motionIconVariants,\n  initial: \"initial\",\n  animate: \"animate\",\n  exit: \"exit\",\n  transition: { duration: 0.25, ease: \"easeOut\" },\n};\n\nexport type CopyStateIconProps = {\n  state: CopyState;\n  idleIcon?: React.ReactNode;\n  doneIcon?: React.ReactNode;\n  errorIcon?: React.ReactNode;\n};\n\nexport function CopyStateIcon({ state, idleIcon, doneIcon, errorIcon }: CopyStateIconProps) {\n  return (\n    <AnimatePresence mode=\"wait\" initial={false}>\n      {state === \"idle\" ? (\n        <motion.span key=\"idle\" {...motionIconProps}>\n          {idleIcon ?? <CopyIcon />}\n        </motion.span>\n      ) : state === \"done\" ? (\n        <motion.span key=\"done\" {...motionIconProps}>\n          {doneIcon ?? <CheckIcon strokeWidth={3} color=\"green\" />}\n        </motion.span>\n      ) : state === \"error\" ? (\n        <motion.span key=\"error\" {...motionIconProps}>\n          {errorIcon ?? <CircleXIcon />}\n        </motion.span>\n      ) : null}\n    </AnimatePresence>\n  );\n}\n\nexport type CopyButtonProps = ComponentProps<typeof Button> & {\n  text: string | (() => string);\n  onCopySuccess?: (text: string) => void;\n  onCopyError?: (error: Error) => void;\n} & Pick<CopyStateIconProps, \"idleIcon\" | \"doneIcon\" | \"errorIcon\">;\n\nexport function CopyButton({\n  size = \"icon\",\n  variant = \"ghost\",\n  children,\n  text,\n  idleIcon,\n  doneIcon,\n  errorIcon,\n  onClick,\n  onCopySuccess,\n  onCopyError,\n  ...props\n}: CopyButtonProps) {\n  const { state, copy } = useCopyToClipboard({ onCopySuccess, onCopyError });\n\n  return (\n    <Button\n      size={size}\n      variant={variant}\n      onClick={(e) => {\n        void copy(text);\n        onClick?.(e);\n      }}\n      aria-label=\"Copy\"\n      {...props}\n    >\n      <CopyStateIcon state={state} idleIcon={idleIcon} doneIcon={doneIcon} errorIcon={errorIcon} />\n      {children}\n    </Button>\n  );\n}\n",
      "type": "registry:component"
    },
    {
      "path": "hooks/use-copy-to-clipboard.ts",
      "content": "import { useState } from \"react\";\n\nexport type CopyState = \"idle\" | \"done\" | \"error\";\n\ntype UseCopyToClipboardOptions = {\n  onCopySuccess?: (text: string) => void;\n  onCopyError?: (error: Error) => void;\n  timeout?: number;\n};\n\nexport function useCopyToClipboard({ onCopySuccess, onCopyError, timeout = 2000 }: UseCopyToClipboardOptions = {}) {\n  const [state, setState] = useState<CopyState>(\"idle\");\n\n  async function copy(text: string | (() => string)) {\n    const value = typeof text === \"function\" ? text() : text;\n    try {\n      await navigator.clipboard.writeText(value);\n      setState(\"done\");\n      onCopySuccess?.(value);\n    } catch (err) {\n      setState(\"error\");\n      onCopyError?.(err instanceof Error ? err : new Error(String(err)));\n    } finally {\n      setTimeout(() => setState(\"idle\"), timeout);\n    }\n  }\n\n  return { state, copy };\n}\n",
      "type": "registry:hook"
    },
    {
      "path": "lib/highlight-code.ts",
      "content": "import { createHash } from \"node:crypto\";\n\nimport { codeToHtml } from \"shiki\";\n\n// Highlighting is deterministic for a given source string + language, so we can\n// safely memoize the generated HTML in-process and avoid repeating the Shiki work\n// on every request during local dev or while the server stays warm.\nconst highlightCache = new Map<string, string>();\n\nexport async function highlightCode(code: string, language: string = \"tsx\") {\n  // Use the content itself as the cache input so repeated component docs pages\n  // can reuse the exact same highlighted payload without caring which route asked\n  // for it.\n  const cacheKey = createHash(\"sha256\").update(`${language}:${code}`).digest(\"hex\");\n\n  const cached = highlightCache.get(cacheKey);\n\n  if (cached) {\n    return cached;\n  }\n\n  // Shiki runs fine on the server: it tokenizes the source code and returns an\n  // HTML string with theme variables and token spans already applied. That lets\n  // the page ship highlighted markup on first paint instead of waiting for a\n  // client-side effect to colorize plain text after hydration.\n  const html = await codeToHtml(code, {\n    lang: language,\n    themes: { dark: \"github-dark\", light: \"github-light\" },\n    transformers: [\n      {\n        pre(node) {\n          node.properties.class =\n            \"shiki no-scrollbar min-w-0 overflow-x-auto overflow-y-auto overscroll-x-contain overscroll-y-auto p-4 pr-16 text-sm leading-6\";\n        },\n        code(node) {\n          node.properties[\"data-line-numbers\"] = \"\";\n        },\n        line(node) {\n          node.properties[\"data-line\"] = \"\";\n        },\n      },\n    ],\n  });\n\n  // The caller renders this HTML payload directly. We keep the wrapper classes\n  // here so every preview gets the same structure and scrolling behavior from one\n  // place instead of rebuilding that shape at each call site.\n  highlightCache.set(cacheKey, html);\n\n  return html;\n}\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:component"
}