{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "segmented-control",
  "title": "分段选择",
  "description": "方条样式的互斥选项组，选中块在选项间滑动，可带图标或小样，与数值滑块同一套高度。",
  "dependencies": [
    "@base-ui/react",
    "class-variance-authority",
    "motion"
  ],
  "registryDependencies": [
    "tooltip",
    "https://yiui.dev/r/flow-tone.json"
  ],
  "files": [
    {
      "path": "src/registry/ui/segmented-control/segmented-control.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\"\nimport { motion, useReducedMotion } from \"motion/react\"\n\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport { cn } from \"@/lib/utils\"\nimport { useControllableState } from \"@/hooks/use-controllable-state\"\nimport { flowToneStyle, type FlowTone } from \"@/lib/flow-tone\"\n\n/**\n * 外框与数值滑块同一套方条：细边框、小圆角、淡底。\n * 高度表也与数值滑块一致（bar 28 / 32 / 36px，compact 18 / 22 / 24px），放在同一个面板里能对齐。\n */\nconst segmentedControlVariants = cva(\n  \"group/segmented-control relative flex h-(--segmented-control-height) w-full min-w-0 items-stretch overflow-hidden rounded-md border border-edge bg-muted/60 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\",\n        default: \"text-sm\",\n        lg: \"text-sm\",\n      },\n      variant: {\n        bar: \"\",\n        compact: \"text-xs\",\n      },\n    },\n    compoundVariants: [\n      {\n        variant: \"bar\",\n        size: \"sm\",\n        class:\n          \"[--segmented-control-height:1.75rem] pointer-coarse:[--segmented-control-height:2.25rem]\",\n      },\n      {\n        variant: \"bar\",\n        size: \"default\",\n        class:\n          \"[--segmented-control-height:2rem] pointer-coarse:[--segmented-control-height:2.5rem]\",\n      },\n      {\n        variant: \"bar\",\n        size: \"lg\",\n        class:\n          \"[--segmented-control-height:2.25rem] pointer-coarse:[--segmented-control-height:2.75rem]\",\n      },\n      {\n        variant: \"compact\",\n        size: \"sm\",\n        class: \"[--segmented-control-height:1.125rem]\",\n      },\n      {\n        variant: \"compact\",\n        size: \"default\",\n        class: \"[--segmented-control-height:1.375rem]\",\n      },\n      {\n        variant: \"compact\",\n        size: \"lg\",\n        class: \"[--segmented-control-height:1.5rem]\",\n      },\n    ],\n    defaultVariants: {\n      size: \"default\",\n      variant: \"bar\",\n    },\n  },\n)\n\ntype SegmentedControlSize = \"sm\" | \"default\" | \"lg\"\ntype SegmentedControlVariant = \"bar\" | \"compact\"\n\ntype SegmentedControlOption = {\n  value: string\n  label: React.ReactNode\n  /** 文字前的小样：图标、色点、材质方块 */\n  icon?: React.ReactNode\n  disabled?: boolean\n}\n\ntype SegmentedControlProps = Omit<\n  React.ComponentProps<typeof RadioGroup>,\n  \"value\" | \"defaultValue\" | \"onValueChange\" | \"children\"\n> & {\n  options: readonly SegmentedControlOption[]\n  /** 受控值 */\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?: SegmentedControlVariant\n  /**\n   * 尺寸档\n   * @default \"default\"\n   */\n  size?: SegmentedControlSize\n  /** 色调：选中块染上 `--flow-tone` 的淡色；不传时选中块用背景色加描边 */\n  tone?: FlowTone\n  /**\n   * 标记为「已改动」：标签改用前景色，根元素带 `data-modified`\n   * @default false\n   */\n  modified?: boolean\n  /**\n   * 只显示图标：每格为正方形，`label` 给读屏，并在悬停或聚焦时用提示气泡显示\n   * @default false\n   */\n  iconOnly?: boolean\n}\n\n/**\n * 分段选择：一排互斥选项，选中块在选项之间滑动。\n * 语义是单选组（`role=\"radiogroup\"`），方向键在选项间移动并直接选中。\n */\nfunction SegmentedControl({\n  options,\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  label,\n  variant = \"bar\",\n  size = \"default\",\n  tone,\n  modified = false,\n  iconOnly = false,\n  disabled,\n  className,\n  style,\n  ...props\n}: SegmentedControlProps) {\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  // 同一页里可能有多个分段选择，选中块的共享布局 id 必须各自独立\n  const layoutId = React.useId()\n  const reduced = useReducedMotion()\n  const hasTone = tone !== undefined\n\n  return (\n    <RadioGroup\n      {...props}\n      value={value}\n      onValueChange={(next) => setValue(next as string)}\n      disabled={disabled}\n      aria-labelledby={label !== undefined ? labelId : undefined}\n      data-slot=\"segmented-control\"\n      data-variant={variant}\n      data-icon-only={iconOnly ? \"\" : undefined}\n      data-modified={modified ? \"\" : undefined}\n      className={cn(\n        segmentedControlVariants({ size, variant }),\n        \"data-[disabled]:opacity-50\",\n        // 纯图标的格子是正方形，整条按内容收紧，不再铺满一行\n        iconOnly && \"w-fit\",\n        // 放进网格工具栏：去掉外框、圆角和底色，高度跟随格子\n        \"in-data-[slot=grid-toolbar]:h-full! in-data-[slot=grid-toolbar]:rounded-none in-data-[slot=grid-toolbar]:border-0 in-data-[slot=grid-toolbar]:bg-transparent\",\n        className,\n      )}\n      style={{ ...flowToneStyle(tone), ...style }}\n    >\n      {label !== undefined ? (\n        <span\n          id={labelId}\n          data-slot=\"segmented-control-label\"\n          className=\"flex shrink-0 items-center border-e border-edge px-3 whitespace-nowrap text-muted-foreground transition-colors group-data-[modified]/segmented-control:text-foreground\"\n        >\n          {label}\n        </span>\n      ) : null}\n      {/*\n        有标签时，标签与选项之间只用一道整高竖线隔开（与方条输入、方条下拉同一种写法），\n        不再另套一层下凹的槽：外框 → 选中块，只有两层。\n      */}\n      <span\n        data-slot=\"segmented-control-options\"\n        // 内部容器不包含标签；纯图标、工具栏和外部要求按内容定宽（w-max、w-fit）时，\n        // 不能启用尺寸包含，否则内容宽度算作 0，整条会塌成一根线。\n        className={cn(\n          \"relative flex min-w-0 flex-1 items-stretch\",\n          !iconOnly &&\n            \"@container/segments in-data-[slot=grid-toolbar]:[container-type:normal] in-[.w-max]:[container-type:normal] in-[.w-fit]:[container-type:normal]\",\n        )}\n      >\n        {options.map((option) => {\n          const selected = option.value === value\n          const item = (\n            <Radio.Root\n              value={option.value}\n              disabled={option.disabled}\n              data-slot=\"segmented-control-item\"\n              className={cn(\n                \"relative flex min-w-0 cursor-pointer items-center justify-center whitespace-nowrap text-muted-foreground transition-colors outline-none hover:text-foreground focus-visible:z-10 focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:ring-inset data-[checked]:text-foreground data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50\",\n                // 纯图标：正方形格子、不留文字间距，图标才能真正居中\n                iconOnly\n                  ? \"aspect-square flex-none\"\n                  : \"flex-auto gap-1.5 px-3 in-data-[slot=grid-toolbar]:flex-1 @max-[12rem]/segments:px-1.5 @max-[8rem]/segments:px-0.5\",\n                // 放进网格工具栏：每个选项是一格，格子之间虚线分隔\n                \"in-data-[slot=grid-toolbar]:border-s in-data-[slot=grid-toolbar]:border-dashed in-data-[slot=grid-toolbar]:border-edge in-data-[slot=grid-toolbar]:first:border-s-0\",\n              )}\n            >\n              {selected ? (\n                <motion.span\n                  layoutId={layoutId}\n                  aria-hidden=\"true\"\n                  data-slot=\"segmented-control-indicator\"\n                  className={cn(\n                    \"absolute inset-[3px] rounded-sm\",\n                    hasTone\n                      ? \"bg-(--flow-tone)/20 dark:bg-(--flow-tone)/35\"\n                      : // 亮色下白块压在浅灰底上对比太弱，补一圈细描边和略重的阴影\n                        \"bg-background shadow-sm ring-1 ring-foreground/10 dark:bg-foreground/15 dark:shadow-xs dark:ring-0\",\n                    // 放进网格工具栏：选中块铺满整格，没有圆角、阴影和描边\n                    \"in-data-[slot=grid-toolbar]:inset-0 in-data-[slot=grid-toolbar]:rounded-none in-data-[slot=grid-toolbar]:bg-muted! in-data-[slot=grid-toolbar]:shadow-none! in-data-[slot=grid-toolbar]:ring-0!\",\n                  )}\n                  transition={\n                    reduced\n                      ? { duration: 0 }\n                      : { type: \"spring\", bounce: 0.18, visualDuration: 0.25 }\n                  }\n                />\n              ) : null}\n              {option.icon ? (\n                <span\n                  aria-hidden=\"true\"\n                  className=\"relative flex shrink-0 items-center [&_svg:not([class*='size-'])]:size-3.5\"\n                >\n                  {option.icon}\n                </span>\n              ) : null}\n              <span className={iconOnly ? \"sr-only\" : \"relative truncate\"}>\n                {option.label}\n              </span>\n            </Radio.Root>\n          )\n          // 纯图标：悬停或键盘聚焦时用提示气泡显示文字\n          return iconOnly ? (\n            <Tooltip key={option.value}>\n              <TooltipTrigger render={item} />\n              <TooltipContent>{option.label}</TooltipContent>\n            </Tooltip>\n          ) : (\n            <React.Fragment key={option.value}>{item}</React.Fragment>\n          )\n        })}\n      </span>\n    </RadioGroup>\n  )\n}\n\nexport { SegmentedControl, segmentedControlVariants }\nexport type {\n  SegmentedControlOption,\n  SegmentedControlProps,\n  SegmentedControlSize,\n  SegmentedControlVariant,\n}\n",
      "type": "registry:ui",
      "target": "@ui/segmented-control/segmented-control.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"
}
