{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bar-field",
  "title": "方条文本框",
  "description": "方条系列基础文本框，支持标签、图标、后缀和错误提示。",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "src/registry/ui/bar-field/bar-field.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva } from \"class-variance-authority\"\nimport { X } from \"lucide-react\"\nimport { Button } from \"@/components/ui/button\"\nimport { cn } from \"@/lib/utils\"\nimport { useControllableState } from \"@/hooks/use-controllable-state\"\n\n// 方条统一承载工具栏适配：去掉外框和底色，聚焦环画在格子内侧。\nconst barFieldVariants = cva(\n  \"flex w-full min-w-0 cursor-text items-center gap-3 rounded-md border bg-muted/60 px-3 transition-colors 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 in-data-[slot=grid-toolbar]:focus-within:ring-3 in-data-[slot=grid-toolbar]:focus-within:ring-ring/50 in-data-[slot=grid-toolbar]:focus-within:ring-inset\",\n  {\n    variants: {\n      size: {\n        sm: \"h-7 text-base pointer-coarse:h-9 md:text-xs\",\n        default: \"h-8 text-base pointer-coarse:h-10 md:text-sm\",\n        lg: \"h-9 text-base pointer-coarse:h-11 md:text-sm\",\n      },\n    },\n    defaultVariants: { size: \"default\" },\n  },\n)\n\ntype BarFieldProps = Omit<\n  React.ComponentProps<\"input\">,\n  \"size\" | \"value\" | \"defaultValue\"\n> & {\n  /** 条内标签；省略时请提供 aria-label 或 aria-labelledby */\n  label?: React.ReactNode\n  /** 前置装饰图标 */\n  icon?: React.ReactNode\n  /** 后缀单位、快捷键或按钮；按钮尺寸应与字段一致 */\n  suffix?: React.ReactNode\n  /** 条下方右侧操作 */\n  footerAction?: React.ReactNode\n  /** 高度档 @default \"default\" */\n  size?: \"sm\" | \"default\" | \"lg\"\n  /** 外部校验错误 */\n  error?: string\n  /** 条下方说明 */\n  description?: React.ReactNode\n  /** 受控字符串 */\n  value?: string\n  /** 初始字符串 @default \"\" */\n  defaultValue?: string\n  /** 字符串变化回调 */\n  onValueChange?: (value: string) => void\n  /** 输入节点引用 */\n  inputRef?: React.Ref<HTMLInputElement>\n  /**\n   * 有内容时显示清除键；清除只触发 `onValueChange(\"\")`，不触发原生 `onChange`\n   * @default type === \"search\"\n   */\n  clearable?: boolean\n  /** 清除键的读屏名称 @default \"清除\" */\n  clearLabel?: string\n}\n\nfunction BarField({\n  label,\n  icon,\n  suffix,\n  footerAction,\n  size = \"default\",\n  error,\n  description,\n  value: valueProp,\n  defaultValue = \"\",\n  onValueChange,\n  inputRef,\n  clearable: clearableProp,\n  clearLabel = \"清除\",\n  ref,\n  className,\n  id: idProp,\n  onChange,\n  disabled,\n  readOnly,\n  \"aria-describedby\": describedBy,\n  \"aria-invalid\": ariaInvalid,\n  ...props\n}: BarFieldProps) {\n  const autoId = React.useId()\n  const id = idProp ?? autoId\n  const localRef = React.useRef<HTMLInputElement>(null)\n  React.useImperativeHandle(inputRef, () => localRef.current!)\n  React.useImperativeHandle(ref, () => localRef.current!)\n  const [value, setValue] = useControllableState({\n    prop: valueProp,\n    defaultProp: defaultValue,\n    onChange: onValueChange,\n  })\n  React.useEffect(() => {\n    const form = localRef.current?.form\n    if (!form || valueProp !== undefined) return\n    const reset = () => {\n      window.setTimeout(() => setValue(defaultValue))\n    }\n    form.addEventListener(\"reset\", reset)\n    return () => form.removeEventListener(\"reset\", reset)\n  }, [valueProp, defaultValue, setValue, props.form])\n  const invalid =\n    Boolean(error) || ariaInvalid === true || ariaInvalid === \"true\"\n  return (\n    <div\n      data-slot=\"bar-field\"\n      data-size={size}\n      className={cn(\n        \"@container/bar-field flex w-full flex-col gap-1.5\",\n        // 放进网格工具栏：铺满整格\n        \"in-data-[slot=grid-toolbar]:h-full\",\n        className,\n      )}\n    >\n      {label && (\n        <label\n          htmlFor={id}\n          aria-hidden=\"true\"\n          data-slot=\"bar-field-label-above\"\n          className=\"hidden text-xs text-muted-foreground in-data-[slot=grid-toolbar]:hidden! @max-[20rem]/bar-field:block\"\n        >\n          {label}\n        </label>\n      )}\n      <div\n        data-slot=\"bar-field-control\"\n        className={cn(\n          barFieldVariants({ size }),\n          invalid\n            ? \"border-destructive ring-3 ring-destructive/20 dark:border-destructive/50 dark:ring-destructive/40\"\n            : \"border-edge focus-within:border-foreground/25 focus-within:bg-background hover:not-focus-within:bg-muted dark:focus-within:bg-input/30\",\n          disabled && \"cursor-not-allowed opacity-50\",\n          readOnly && \"bg-muted/30\",\n        )}\n        onMouseDown={(event) => {\n          if (event.target === event.currentTarget) {\n            event.preventDefault()\n            localRef.current?.focus()\n          }\n        }}\n      >\n        {label && (\n          <label\n            htmlFor={id}\n            data-slot=\"bar-field-label\"\n            className=\"shrink-0 text-muted-foreground in-data-[slot=grid-toolbar]:not-sr-only! in-data-[slot=grid-toolbar]:whitespace-nowrap! max-md:text-sm @max-[20rem]/bar-field:sr-only\"\n          >\n            {label}\n          </label>\n        )}\n        {icon && (\n          <span\n            aria-hidden=\"true\"\n            data-slot=\"bar-field-icon\"\n            className=\"shrink-0 text-muted-foreground [&_svg]:size-4\"\n          >\n            {icon}\n          </span>\n        )}\n        <input\n          {...props}\n          ref={localRef}\n          id={id}\n          disabled={disabled}\n          readOnly={readOnly}\n          value={value}\n          aria-invalid={invalid || undefined}\n          aria-describedby={\n            [\n              describedBy,\n              error && `${id}-error`,\n              description && `${id}-description`,\n            ]\n              .filter(Boolean)\n              .join(\" \") || undefined\n          }\n          data-slot=\"bar-field-input\"\n          className={cn(\n            \"h-full min-w-0 flex-1 bg-transparent font-medium outline-none placeholder:font-normal placeholder:text-muted-foreground disabled:cursor-not-allowed\",\n            // 隐藏浏览器自带的搜索清除钮，统一用上面的自绘清除键\n            \"[&::-webkit-search-cancel-button]:appearance-none\",\n            // 有标签时值靠右（方条惯例）；带前置图标的是搜索类输入，文字紧跟图标从左写起\n            label && !icon && \"text-right @max-[20rem]/bar-field:text-left\",\n          )}\n          onChange={(event) => {\n            setValue(event.target.value)\n            onChange?.(event)\n          }}\n        />\n        {/* 自绘清除键：与方条下拉的清除键同一档，替代浏览器自带的搜索清除钮 */}\n        {(clearableProp ?? props.type === \"search\") &&\n          value !== \"\" &&\n          !disabled &&\n          !readOnly && (\n            <Button\n              type=\"button\"\n              size=\"icon-xs\"\n              variant=\"ghost\"\n              data-slot=\"bar-field-clear\"\n              aria-label={clearLabel}\n              className=\"-mx-1.5 shrink-0 text-muted-foreground hover:text-foreground\"\n              onClick={() => {\n                setValue(\"\")\n                localRef.current?.focus()\n              }}\n            >\n              <X className=\"size-3.5\" />\n            </Button>\n          )}\n        {suffix && (\n          <span\n            data-slot=\"bar-field-suffix\"\n            className=\"-mr-3 flex shrink-0 items-center self-stretch pr-2 text-muted-foreground [&_button]:max-h-full [&_button]:rounded-none [&_button]:focus-visible:ring-inset\"\n          >\n            {suffix}\n          </span>\n        )}\n      </div>\n      {(error || description || footerAction) && (\n        <div\n          data-slot=\"bar-field-footer\"\n          className=\"flex items-start justify-between gap-3 text-xs\"\n        >\n          <div>\n            {error && (\n              <p\n                id={`${id}-error`}\n                data-slot=\"bar-field-error\"\n                className=\"text-destructive\"\n              >\n                {error}\n              </p>\n            )}\n            {description && (\n              <p id={`${id}-description`} className=\"text-muted-foreground\">\n                {description}\n              </p>\n            )}\n          </div>\n          {footerAction}\n        </div>\n      )}\n    </div>\n  )\n}\nexport { BarField, barFieldVariants }\nexport type { BarFieldProps }\n",
      "type": "registry:ui",
      "target": "@ui/bar-field/bar-field.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"
}
