{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "control-panel",
  "title": "控制面板",
  "description": "承载方条控件的面板容器：标题栏、分组网格、恢复按钮，以及把当前参数复制为 JSX 的按钮。",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "src/registry/ui/control-panel/control-panel.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check, Copy, RotateCcw } from \"lucide-react\"\n\nimport { buttonVariants } from \"@/components/ui/button\"\nimport { cn } from \"@/lib/utils\"\n\n/**\n * 控制面板：承载数值滑块、分段选择、开关、色调选择这一套方条控件的容器。\n * 由标题栏（标题 + 操作）和若干分组组成，分组内是等宽网格；复制按钮把当前参数写成 JSX。\n */\nfunction ControlPanel({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"control-panel\"\n      className={cn(\n        \"flex w-full min-w-0 flex-col rounded-lg border border-edge bg-background text-sm\",\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction ControlPanelHeader({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"control-panel-header\"\n      className={cn(\n        \"flex h-10 shrink-0 items-center gap-2 border-b border-edge ps-3 pe-1.5\",\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction ControlPanelTitle({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"control-panel-title\"\n      className={cn(\n        \"truncate text-xs font-medium text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\n/** 标题栏右侧的操作区，放复制、恢复等图标按钮 */\nfunction ControlPanelActions({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"control-panel-actions\"\n      className={cn(\"ms-auto flex shrink-0 items-center gap-0.5\", className)}\n      {...props}\n    />\n  )\n}\n\ntype ControlPanelGroupProps = React.ComponentProps<\"div\"> & {\n  /** 分组小标题；不传时不显示 */\n  title?: React.ReactNode\n  /**\n   * 宽屏下的列数，窄屏（< 640px）一律单列。子控件用 `sm:col-span-2` 之类跨列\n   * @default 1\n   */\n  columns?: 1 | 2 | 3\n}\n\nconst COLUMNS = {\n  1: \"\",\n  2: \"sm:grid-cols-2\",\n  3: \"sm:grid-cols-3\",\n} as const\n\n/** 一组控件：可选的小标题 + 网格。相邻分组之间自动画一条分隔线。 */\nfunction ControlPanelGroup({\n  title,\n  columns = 1,\n  className,\n  children,\n  ...props\n}: ControlPanelGroupProps) {\n  const titleId = React.useId()\n  return (\n    <div\n      role=\"group\"\n      aria-labelledby={title !== undefined ? titleId : undefined}\n      data-slot=\"control-panel-group\"\n      className={cn(\n        \"flex flex-col gap-2.5 px-4 pt-3 pb-4 [[data-slot=control-panel-group]+&]:border-t [[data-slot=control-panel-group]+&]:border-edge\",\n        className,\n      )}\n      {...props}\n    >\n      {title !== undefined ? (\n        <span\n          id={titleId}\n          data-slot=\"control-panel-group-title\"\n          className=\"text-xs text-muted-foreground\"\n        >\n          {title}\n        </span>\n      ) : null}\n      <div\n        data-slot=\"control-panel-grid\"\n        className={cn(\"grid gap-x-4 gap-y-3\", COLUMNS[columns])}\n      >\n        {children}\n      </div>\n    </div>\n  )\n}\n\ntype ControlPanelCopyProps = Omit<\n  React.ComponentProps<\"button\">,\n  \"onClick\" | \"children\"\n> & {\n  /** 要复制的文本；传函数时在点击那一刻才求值 */\n  code: string | (() => string)\n  /**\n   * 按钮的可访问名称\n   * @default \"复制代码\"\n   */\n  label?: string\n  /**\n   * 复制成功后的提示（读屏会朗读）\n   * @default \"已复制\"\n   */\n  copiedLabel?: string\n}\n\n/** 复制按钮：成功后图标换成对勾 1.5 秒。剪贴板不可用（非安全上下文）时静默失败。 */\nfunction ControlPanelCopy({\n  code,\n  label = \"复制代码\",\n  copiedLabel = \"已复制\",\n  className,\n  ...props\n}: ControlPanelCopyProps) {\n  const [copied, setCopied] = React.useState(false)\n\n  React.useEffect(() => {\n    if (!copied) return\n    const timer = window.setTimeout(() => setCopied(false), 1500)\n    return () => window.clearTimeout(timer)\n  }, [copied])\n\n  const copy = async () => {\n    try {\n      await navigator.clipboard.writeText(\n        typeof code === \"function\" ? code() : code,\n      )\n      setCopied(true)\n    } catch {\n      // 非 https 或用户拒绝授权时拿不到剪贴板，不打断操作\n    }\n  }\n\n  return (\n    <button\n      type=\"button\"\n      aria-label={label}\n      title={label}\n      data-slot=\"control-panel-copy\"\n      data-copied={copied ? \"\" : undefined}\n      onClick={copy}\n      className={cn(\n        buttonVariants({ variant: \"ghost\", size: \"icon-sm\" }),\n        \"text-muted-foreground hover:text-foreground\",\n        className,\n      )}\n      {...props}\n    >\n      {copied ? <Check /> : <Copy />}\n      <span aria-live=\"polite\" className=\"sr-only\">\n        {copied ? copiedLabel : \"\"}\n      </span>\n    </button>\n  )\n}\n\ntype ControlPanelResetProps = React.ComponentProps<\"button\"> & {\n  /**\n   * 按钮的可访问名称\n   * @default \"恢复默认\"\n   */\n  label?: string\n}\n\nfunction ControlPanelReset({\n  label = \"恢复默认\",\n  className,\n  ...props\n}: ControlPanelResetProps) {\n  return (\n    <button\n      type=\"button\"\n      aria-label={label}\n      title={label}\n      data-slot=\"control-panel-reset\"\n      className={cn(\n        buttonVariants({ variant: \"ghost\", size: \"icon-sm\" }),\n        \"text-muted-foreground hover:text-foreground\",\n        className,\n      )}\n      {...props}\n    >\n      <RotateCcw />\n    </button>\n  )\n}\n\nexport {\n  ControlPanel,\n  ControlPanelActions,\n  ControlPanelCopy,\n  ControlPanelGroup,\n  ControlPanelHeader,\n  ControlPanelReset,\n  ControlPanelTitle,\n}\nexport type {\n  ControlPanelCopyProps,\n  ControlPanelGroupProps,\n  ControlPanelResetProps,\n}\nexport { formatJsx } from \"./format-jsx\"\nexport type { FormatJsxOptions } from \"./format-jsx\"\n",
      "type": "registry:ui",
      "target": "@ui/control-panel/control-panel.tsx"
    },
    {
      "path": "src/registry/ui/control-panel/format-jsx.ts",
      "content": "/**\n * 把组件名和一组属性写成 JSX 字符串，用于控制面板的「复制为代码」。\n * - `undefined` / `null` 的属性省略；`true` 写成裸属性名，`false` 写成 `{false}`\n * - 字符串优先用双引号，含双引号或换行时改成 `{\"...\"}`\n * - 数字去掉浮点尾差（最多 4 位小数）；数组与对象用 JSON\n * - 一行放得下（不超过 `width` 个字符）就写一行，否则每个属性一行\n */\ntype FormatJsxOptions = {\n  /**\n   * 单行的最大宽度\n   * @default 72\n   */\n  width?: number\n  /** 写在标签里的子内容；不传时输出自闭合标签 */\n  children?: string\n}\n\nfunction formatProp(name: string, value: unknown): string | null {\n  if (value === undefined || value === null) return null\n  if (value === true) return name\n  if (typeof value === \"string\") {\n    return /[\"\\n]/.test(value)\n      ? `${name}={${JSON.stringify(value)}}`\n      : `${name}=\"${value}\"`\n  }\n  if (typeof value === \"number\") {\n    return `${name}={${Number(value.toFixed(4))}}`\n  }\n  return `${name}={${JSON.stringify(value)}}`\n}\n\nfunction formatJsx(\n  component: string,\n  props: Record<string, unknown>,\n  { width = 72, children }: FormatJsxOptions = {},\n) {\n  const parts = Object.entries(props)\n    .map(([name, value]) => formatProp(name, value))\n    .filter((part): part is string => part !== null)\n\n  const close = children === undefined ? \" />\" : `>${children}</${component}>`\n  const inline = `<${component}${parts.map((part) => ` ${part}`).join(\"\")}${close}`\n  if (inline.length <= width || parts.length === 0) return inline\n\n  const lines = parts.map((part) => `  ${part}`).join(\"\\n\")\n  return children === undefined\n    ? `<${component}\\n${lines}\\n/>`\n    : `<${component}\\n${lines}\\n>\\n  ${children}\\n</${component}>`\n}\n\nexport { formatJsx }\nexport type { FormatJsxOptions }\n",
      "type": "registry:ui",
      "target": "@ui/control-panel/format-jsx.ts"
    }
  ],
  "cssVars": {
    "theme": {
      "color-edge": "var(--edge)"
    },
    "light": {
      "edge": "color-mix(in oklch, var(--foreground) 12%, transparent)"
    },
    "dark": {
      "edge": "color-mix(in oklch, var(--foreground) 18%, transparent)"
    }
  },
  "type": "registry:ui"
}
