{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tone-picker",
  "title": "色调选择",
  "description": "方条左侧标签、右侧一排色点的单选组，色点取主题变量，跟随亮暗与颜色主题。",
  "dependencies": [
    "@base-ui/react",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "https://yiui.dev/r/flow-tone.json"
  ],
  "files": [
    {
      "path": "src/registry/ui/tone-picker/tone-picker.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Radio } from \"@base-ui/react/radio\"\nimport { RadioGroup } from \"@base-ui/react/radio-group\"\nimport { cva } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useControllableState } from \"@/hooks/use-controllable-state\"\nimport { flowToneStyle, type FlowTone } from \"@/lib/flow-tone\"\n\n/**\n * 与数值滑块同一套方条与高度表（bar 28 / 32 / 36px，compact 18 / 22 / 24px）。\n * 触屏设备上 bar 各加高一档（36 / 40 / 44px），色点放大、间距拉开，每个色点外围还有一圈看不见的点按区。\n */\nconst tonePickerVariants = cva(\n  \"group/tone-picker flex h-(--tone-picker-height) w-full min-w-0 items-center justify-between gap-3 rounded-md border border-edge bg-muted/60 px-3 select-none aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40\",\n  {\n    variants: {\n      size: {\n        sm: \"text-xs [--tone-picker-swatch:0.75rem]\",\n        default: \"text-sm [--tone-picker-swatch:0.875rem]\",\n        lg: \"text-sm [--tone-picker-swatch:1rem]\",\n      },\n      variant: {\n        bar: \"\",\n        compact: \"text-xs [--tone-picker-swatch:0.625rem]\",\n      },\n    },\n    compoundVariants: [\n      {\n        variant: \"bar\",\n        size: \"sm\",\n        class:\n          \"[--tone-picker-height:1.75rem] pointer-coarse:[--tone-picker-height:2.25rem]\",\n      },\n      {\n        variant: \"bar\",\n        size: \"default\",\n        class:\n          \"[--tone-picker-height:2rem] pointer-coarse:[--tone-picker-height:2.5rem]\",\n      },\n      {\n        variant: \"bar\",\n        size: \"lg\",\n        class:\n          \"[--tone-picker-height:2.25rem] pointer-coarse:[--tone-picker-height:2.75rem]\",\n      },\n      {\n        variant: \"compact\",\n        size: \"sm\",\n        class: \"[--tone-picker-height:1.125rem]\",\n      },\n      {\n        variant: \"compact\",\n        size: \"default\",\n        class: \"[--tone-picker-height:1.375rem]\",\n      },\n      {\n        variant: \"compact\",\n        size: \"lg\",\n        class: \"[--tone-picker-height:1.5rem]\",\n      },\n    ],\n    defaultVariants: {\n      size: \"default\",\n      variant: \"bar\",\n    },\n  },\n)\n\ntype TonePickerSize = \"sm\" | \"default\" | \"lg\"\ntype TonePickerVariant = \"bar\" | \"compact\"\n\ntype TonePickerOption = {\n  value: string\n  /** 可访问名称，悬停时也作为提示 */\n  label: string\n  /** 色点的颜色：内置色调名或任意 CSS 颜色；`null` 画成空心圆（表示「不上色」「沿用」这类选项） */\n  tone: FlowTone | null\n}\n\n/** 内置色调的中文名 */\nconst TONE_LABELS: Record<string, string> = {\n  primary: \"主题色\",\n  foreground: \"前景色\",\n  blue: \"蓝\",\n  green: \"绿\",\n  amber: \"琥珀\",\n  rose: \"玫红\",\n  violet: \"紫\",\n  cyan: \"青\",\n}\n\nconst DEFAULT_TONES: FlowTone[] = [\n  \"primary\",\n  \"blue\",\n  \"green\",\n  \"amber\",\n  \"rose\",\n  \"violet\",\n  \"cyan\",\n]\n\n/** 把色调名列表变成选项；`TonePicker` 不传 `options` 时用的就是它 */\nfunction toneOptions(tones: readonly FlowTone[] = DEFAULT_TONES) {\n  return tones.map<TonePickerOption>((tone) => ({\n    value: tone,\n    label: TONE_LABELS[tone] ?? tone,\n    tone,\n  }))\n}\n\ntype TonePickerProps = Omit<\n  React.ComponentProps<typeof RadioGroup>,\n  \"value\" | \"defaultValue\" | \"onValueChange\" | \"children\"\n> & {\n  /** 可选的色调；不传时是主题色加六种多彩色调 */\n  options?: readonly TonePickerOption[]\n  value?: string\n  /** 非受控时的初始值；不传时取第一项 */\n  defaultValue?: string\n  onValueChange?: (value: string) => void\n  /** 左侧标签，同时作为单选组的可访问名称 */\n  label?: React.ReactNode\n  /**\n   * 高度档：`bar` 与数值滑块默认同高，`compact` 是面板里用的中等高度\n   * @default \"bar\"\n   */\n  variant?: TonePickerVariant\n  /**\n   * 尺寸档\n   * @default \"default\"\n   */\n  size?: TonePickerSize\n  /**\n   * 标记为「已改动」：标签改用前景色，根元素带 `data-modified`\n   * @default false\n   */\n  modified?: boolean\n  /**\n   * 色点按固定列数排成网格（选项多、一行放不下时用），方条随行数加高\n   */\n  columns?: number\n}\n\n/**\n * 色调选择：方条左侧标签、右侧一排色点，单选语义，方向键切换。\n * 色点颜色都来自主题变量，亮暗与颜色主题切换时跟着变。\n */\nfunction TonePicker({\n  options: optionsProp,\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  label,\n  variant = \"bar\",\n  size = \"default\",\n  modified = false,\n  columns,\n  className,\n  ...props\n}: TonePickerProps) {\n  const options = optionsProp ?? toneOptions()\n  const [value, setValue] = useControllableState<string>({\n    prop: valueProp,\n    defaultProp: defaultValue ?? options[0]?.value ?? \"\",\n    onChange: onValueChange,\n  })\n  const labelId = React.useId()\n\n  return (\n    <RadioGroup\n      {...props}\n      value={value}\n      onValueChange={(next) => setValue(next as string)}\n      aria-labelledby={label !== undefined ? labelId : undefined}\n      data-slot=\"tone-picker\"\n      data-variant={variant}\n      data-modified={modified ? \"\" : undefined}\n      className={cn(\n        tonePickerVariants({ size, variant }),\n        \"data-[disabled]:opacity-50\",\n        columns && \"h-auto min-h-(--tone-picker-height) py-2.5\",\n        className,\n      )}\n    >\n      {label !== undefined ? (\n        <span\n          id={labelId}\n          data-slot=\"tone-picker-label\"\n          className=\"truncate font-medium text-muted-foreground transition-colors group-data-[modified]/tone-picker:text-foreground\"\n        >\n          {label}\n        </span>\n      ) : null}\n      <span\n        data-slot=\"tone-picker-swatches\"\n        style={\n          columns\n            ? { gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }\n            : undefined\n        }\n        className={cn(\n          \"pointer-coarse:group-data-[variant=bar]/tone-picker:[--tone-picker-swatch:1.125rem]\",\n          columns\n            ? \"grid w-full justify-items-center gap-y-3 pointer-coarse:gap-y-4\"\n            : \"ms-auto flex shrink-0 items-center gap-1.5 pointer-coarse:group-data-[variant=bar]/tone-picker:gap-3\",\n        )}\n      >\n        {options.map((option) => (\n          <Radio.Root\n            key={option.value}\n            value={option.value}\n            aria-label={option.label}\n            title={option.label}\n            data-slot=\"tone-picker-swatch\"\n            className={cn(\n              \"relative size-(--tone-picker-swatch) shrink-0 cursor-pointer rounded-full transition-transform outline-none before:absolute before:-inset-x-[3px] before:-inset-y-2 before:content-[''] hover:scale-110 focus-visible:outline-[3px] focus-visible:outline-offset-4 focus-visible:outline-ring focus-visible:outline-solid motion-reduce:transition-none motion-reduce:hover:scale-100 pointer-coarse:before:-inset-x-1.5 pointer-coarse:before:-inset-y-4\",\n              // 选中：外面再套一圈前景色细环，和色点之间隔一道背景色缝\n              \"data-[checked]:ring-2 data-[checked]:ring-foreground/70 data-[checked]:ring-offset-2 data-[checked]:ring-offset-background\",\n              option.tone === null\n                ? \"border border-dashed border-muted-foreground/60\"\n                : \"bg-(--flow-tone)\",\n            )}\n            style={flowToneStyle(option.tone ?? undefined)}\n          />\n        ))}\n      </span>\n    </RadioGroup>\n  )\n}\n\nexport { TonePicker, tonePickerVariants, toneOptions }\nexport type {\n  TonePickerOption,\n  TonePickerProps,\n  TonePickerSize,\n  TonePickerVariant,\n}\n",
      "type": "registry:ui",
      "target": "@ui/tone-picker/tone-picker.tsx"
    },
    {
      "path": "src/registry/hooks/use-controllable-state.ts",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\ntype UseControllableStateParams<T> = {\n  /** 受控值；传入后组件不再维护内部状态 */\n  prop?: T\n  /** 非受控时的初始值 */\n  defaultProp: T\n  /** 值变化时回调（受控与非受控都会触发） */\n  onChange?: (value: T) => void\n}\n\n/**\n * 同时支持受控与非受控的状态 hook。\n * 约定见 component-conventions 技能第 4 节：open/defaultOpen/onOpenChange 等成对 props 都用它实现。\n */\nfunction useControllableState<T>({\n  prop,\n  defaultProp,\n  onChange,\n}: UseControllableStateParams<T>) {\n  const [uncontrolled, setUncontrolled] = React.useState(defaultProp)\n  const isControlled = prop !== undefined\n  const value = isControlled ? prop : uncontrolled\n\n  // 用 ref 保存最新的回调和值，保证 setValue 引用稳定\n  const onChangeRef = React.useRef(onChange)\n  const valueRef = React.useRef(value)\n  React.useEffect(() => {\n    onChangeRef.current = onChange\n    valueRef.current = value\n  })\n\n  const setValue = React.useCallback(\n    (next: React.SetStateAction<T>) => {\n      const resolved =\n        typeof next === \"function\"\n          ? (next as (prev: T) => T)(valueRef.current)\n          : next\n      if (Object.is(resolved, valueRef.current)) return\n      valueRef.current = resolved\n      if (!isControlled) setUncontrolled(resolved)\n      onChangeRef.current?.(resolved)\n    },\n    [isControlled],\n  )\n\n  return [value, setValue] as const\n}\n\nexport { useControllableState }\n",
      "type": "registry:ui",
      "target": "hooks/use-controllable-state.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"
}
