{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "phone-input",
  "title": "手机号输入",
  "description": "中国大陆手机号输入：边输边按 3-4-4 分隔且光标不乱跳，粘贴、自动填充带 +86 或横线的号码自动整理，失焦校验号段；条内可放带倒计时的「获取验证码」。值是 11 位纯数字。",
  "registryDependencies": [
    "button",
    "https://yiui.dev/r/bar-field.json"
  ],
  "files": [
    {
      "path": "src/registry/ui/phone-input/phone-input.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useCaretRestore } from \"@/hooks/use-caret-restore\"\nimport { useControllableState } from \"@/hooks/use-controllable-state\"\n\nimport { DEFAULT_LABELS, PHONE_LENGTH } from \"./config\"\nimport {\n  PhoneInputContext,\n  PhoneInputSendButton,\n  phoneInputControlVariants,\n  useSendCode,\n} from \"./phone-input-parts\"\nimport type { PhoneInputProps } from \"./types\"\nimport {\n  digitsOf,\n  formatPhone,\n  isValidPhone,\n  maskPhone,\n  sanitizePhone,\n} from \"./utils\"\n\n/**\n * 中国大陆手机号输入：边输边按 3-4-4 分隔，光标不乱跳；粘贴、自动填充带区号或横线的号码也能整理。\n * 值是 11 位纯数字，表单提交的也是它。条内右侧可以放「获取验证码」。\n */\nfunction PhoneInput({\n  value: valueProp,\n  defaultValue = \"\",\n  onValueChange,\n  label,\n  size = \"default\",\n  action,\n  error,\n  labels: labelsProp,\n  className,\n  id: idProp,\n  name,\n  form,\n  required,\n  disabled,\n  readOnly,\n  placeholder,\n  \"aria-invalid\": ariaInvalid,\n  \"aria-describedby\": describedByProp,\n  onFocus,\n  onBlur,\n  ref,\n  ...props\n}: PhoneInputProps) {\n  const labels = { ...DEFAULT_LABELS, ...labelsProp }\n  const autoId = React.useId()\n  const id = idProp ?? autoId\n  const errorId = `${id}-error`\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  React.useImperativeHandle(ref, () => inputRef.current as HTMLInputElement)\n\n  const [value, setValue] = useControllableState({\n    prop: valueProp,\n    defaultProp: digitsOf(defaultValue).slice(0, PHONE_LENGTH),\n    onChange: onValueChange,\n  })\n  const text = formatPhone(value)\n  const [touched, setTouched] = React.useState(false)\n\n  // 整理后要恢复的光标位置；每次都是新对象，保证文本没变（敲了非法字符）时也会重设光标\n  const setCaret = useCaretRestore(inputRef)\n\n  const commit = (raw: string, rawCaret: number) => {\n    const next = sanitizePhone(raw, rawCaret)\n    setValue(next.digits)\n    setCaret({ position: next.caret })\n  }\n\n  const valid = isValidPhone(value)\n  const ownMessage =\n    value === \"\"\n      ? required\n        ? labels.required\n        : \"\"\n      : valid\n        ? \"\"\n        : labels.invalid\n  // 浏览器的表单校验也拦住：号码不完整时提交会被挡下\n  React.useEffect(() => {\n    inputRef.current?.setCustomValidity(ownMessage)\n  }, [ownMessage])\n\n  const errorMessage = error || (touched && value !== \"\" ? ownMessage : \"\")\n  const invalid =\n    Boolean(errorMessage) || ariaInvalid === true || ariaInvalid === \"true\"\n  const describedBy =\n    [describedByProp, errorMessage ? errorId : undefined]\n      .filter(Boolean)\n      .join(\" \") || undefined\n\n  // 非受控时表单重置回到初始值；reset 事件在表单清空之前触发，等一帧再覆盖\n  const controlled = valueProp !== undefined\n  const resetRef = React.useRef(() => {})\n  React.useEffect(() => {\n    resetRef.current = () => {\n      setValue(digitsOf(defaultValue).slice(0, PHONE_LENGTH))\n      setTouched(false)\n    }\n  })\n  React.useEffect(() => {\n    const element = inputRef.current?.form\n    if (!element || controlled) return\n    const reset = () => window.setTimeout(() => resetRef.current())\n    element.addEventListener(\"reset\", reset)\n    return () => element.removeEventListener(\"reset\", reset)\n  }, [controlled])\n\n  return (\n    <div\n      data-slot=\"phone-input\"\n      data-size={size}\n      data-invalid={invalid ? \"\" : undefined}\n      data-disabled={disabled ? \"\" : undefined}\n      // 容器查询：窄于 20rem 时缩小间距和按钮内边距，并把标签挪到条上方，号码不被挤掉\n      className={cn(\n        \"@container/phone-input in-data-[slot=grid-toolbar]:h-full flex w-full flex-col gap-1.5\",\n        className,\n      )}\n    >\n      {/* 窄于 20rem 时条内放不下「标签 + 号码」：标签挪到条上方显示，条内那份只留给读屏。\n          这一份 aria-hidden，只负责看和点（点它聚焦输入框），可访问名称仍来自条内的 label */}\n      {label && (\n        <label\n          aria-hidden=\"true\"\n          htmlFor={id}\n          data-slot=\"phone-input-label-above\"\n          className=\"hidden text-xs text-muted-foreground @max-[20rem]/phone-input:block in-data-[slot=grid-toolbar]:hidden!\"\n        >\n          {label}\n        </label>\n      )}\n      <div\n        data-slot=\"phone-input-control\"\n        className={cn(\n          phoneInputControlVariants({ size }),\n          \"@max-[20rem]/phone-input:gap-2\",\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          action && \"overflow-hidden\",\n        )}\n        // 点到条内空白处（标签与号码之间）也聚焦输入框\n        onMouseDown={(event) => {\n          if (event.target === event.currentTarget) {\n            event.preventDefault()\n            inputRef.current?.focus()\n          }\n        }}\n      >\n        {label && (\n          <label\n            data-slot=\"phone-input-label\"\n            htmlFor={id}\n            className=\"shrink-0 text-muted-foreground max-md:text-sm @max-[20rem]/phone-input:sr-only in-data-[slot=grid-toolbar]:not-sr-only! in-data-[slot=grid-toolbar]:whitespace-nowrap!\"\n          >\n            {label}\n          </label>\n        )}\n        <input\n          {...props}\n          ref={inputRef}\n          id={id}\n          data-slot=\"phone-input-input\"\n          // tel + numeric：iOS 弹纯数字键盘，自动填充也认得这是电话号码\n          type=\"tel\"\n          inputMode=\"numeric\"\n          autoComplete=\"tel-national\"\n          spellCheck={false}\n          value={text}\n          placeholder={placeholder ?? labels.placeholder}\n          disabled={disabled}\n          readOnly={readOnly}\n          required={required}\n          aria-invalid={invalid || undefined}\n          aria-describedby={describedBy}\n          className={cn(\n            \"h-full min-w-0 flex-1 bg-transparent font-medium tabular-nums outline-none placeholder:font-normal placeholder:text-muted-foreground disabled:cursor-not-allowed\",\n            // 有条内标签时号码靠右，与方条开关、金额输入的「左标签右数值」一致\n            label && \"text-right @max-[20rem]/phone-input:text-left\",\n          )}\n          onChange={(event) => {\n            const input = event.currentTarget\n            commit(input.value, input.selectionStart ?? input.value.length)\n          }}\n          onKeyDown={(event) => {\n            props.onKeyDown?.(event)\n            const input = event.currentTarget\n            const { selectionStart: start, selectionEnd: end } = input\n            if (\n              event.defaultPrevented ||\n              readOnly ||\n              start === null ||\n              start !== end\n            )\n              return\n            // 光标紧贴分隔空格时，退格 / 删除越过空格去删旁边的数字，否则删了空格又长回来\n            const current = input.value\n            if (event.key === \"Backspace\" && current[start - 1] === \" \") {\n              event.preventDefault()\n              commit(\n                current.slice(0, start - 2) + current.slice(start),\n                start - 2,\n              )\n            } else if (event.key === \"Delete\" && current[start] === \" \") {\n              event.preventDefault()\n              commit(current.slice(0, start) + current.slice(start + 2), start)\n            }\n          }}\n          onFocus={onFocus}\n          onBlur={(event) => {\n            setTouched(true)\n            onBlur?.(event)\n          }}\n        />\n        {action && (\n          <PhoneInputContext.Provider value={{ valid }}>\n            {action}\n          </PhoneInputContext.Provider>\n        )}\n      </div>\n      {errorMessage && (\n        <p\n          id={errorId}\n          data-slot=\"phone-input-error\"\n          className=\"text-xs text-destructive\"\n        >\n          {errorMessage}\n        </p>\n      )}\n      {name && (\n        <input\n          type=\"hidden\"\n          data-slot=\"phone-input-value\"\n          name={name}\n          form={form}\n          value={value}\n          disabled={disabled}\n        />\n      )}\n    </div>\n  )\n}\n\nexport {\n  PhoneInput,\n  PhoneInputSendButton,\n  formatPhone,\n  isValidPhone,\n  maskPhone,\n  phoneInputControlVariants,\n  useSendCode,\n}\nexport type {\n  PhoneInputProps,\n  PhoneInputSendButtonProps,\n  PhoneInputSize,\n} from \"./types\"\n",
      "type": "registry:ui",
      "target": "@ui/phone-input/phone-input.tsx"
    },
    {
      "path": "src/registry/ui/phone-input/phone-input-parts.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { barFieldVariants } from \"@/components/ui/bar-field/bar-field\"\n\nimport { buttonVariants } from \"@/components/ui/button\"\nimport { cn } from \"@/lib/utils\"\n\nimport { DEFAULT_COOLDOWN, DEFAULT_LABELS } from \"./config\"\nimport type { PhoneInputSendButtonProps } from \"./types\"\n\n/**\n * 手机号方条：与方条开关、金额输入同一套外观（浅灰底、细边框、标签在条内左侧）。\n * 高度只来自既有三档 28 / 32 / 36px，触屏设备上各加高一档（36 / 40 / 44px）；\n * 手机上字号保持 16px，iOS 聚焦时才不会自动放大页面\n */\nconst phoneInputControlVariants = barFieldVariants\n\n/** 手机号方条把「号码是否有效」告诉条内的发送按钮：号码不完整时按钮自动置灰 */\nconst PhoneInputContext = React.createContext<{ valid: boolean } | null>(null)\n\ntype SendState = \"idle\" | \"sending\" | \"counting\" | \"again\"\n\n/**\n * 「获取验证码」的状态：发送中 → 倒计时 → 可以重新获取。\n * 剩余秒数按截止时间算，页面切到后台计时器变慢也不会多等。\n */\nfunction useSendCode(onSend: () => void | Promise<void>, cooldown: number) {\n  const [state, setState] = React.useState<SendState>(\"idle\")\n  const [deadline, setDeadline] = React.useState(0)\n  const [now, setNow] = React.useState(0)\n\n  React.useEffect(() => {\n    if (state !== \"counting\") return\n    const timer = window.setInterval(() => {\n      const current = Date.now()\n      setNow(current)\n      if (current >= deadline) setState(\"again\")\n    }, 250)\n    return () => window.clearInterval(timer)\n  }, [state, deadline])\n\n  const send = async () => {\n    if (state === \"sending\" || state === \"counting\") return\n    const previous = state\n    setState(\"sending\")\n    try {\n      await onSend()\n    } catch {\n      // 发送失败不进入倒计时，用户可以马上重试；错误提示由调用方处理\n      setState(previous)\n      return\n    }\n    const start = Date.now()\n    setNow(start)\n    setDeadline(start + cooldown * 1000)\n    setState(\"counting\")\n  }\n\n  const remaining = Math.max(0, Math.ceil((deadline - now) / 1000))\n  return { state, remaining, send }\n}\n\n/** 获取验证码按钮：放进手机号方条的 `action`，也可以单独用在邮箱等别的输入旁边 */\nfunction PhoneInputSendButton({\n  onSend,\n  cooldown = DEFAULT_COOLDOWN,\n  variant = \"inline\",\n  size = \"default\",\n  labels: labelsProp,\n  disabled,\n  className,\n  ...props\n}: PhoneInputSendButtonProps) {\n  const labels = { ...DEFAULT_LABELS, ...labelsProp }\n  const context = React.useContext(PhoneInputContext)\n  const { state, remaining, send } = useSendCode(onSend, cooldown)\n  const busy = state === \"sending\" || state === \"counting\"\n  const text =\n    state === \"sending\"\n      ? labels.sending\n      : state === \"counting\"\n        ? labels.countdown.replace(\"{seconds}\", String(remaining))\n        : state === \"again\"\n          ? labels.resend\n          : labels.send\n  // 四种状态的文字长短不一，宽度会跟着跳。把它们都叠在同一格里（只显示当前这一个），\n  // 按钮宽度取最长的那个；倒计时用冷却秒数占位（位数最多），数字是等宽的\n  const sizers = [\n    labels.send,\n    labels.sending,\n    labels.countdown.replace(\"{seconds}\", String(cooldown)),\n    labels.resend,\n  ]\n\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"phone-input-send\"\n      data-state={state}\n      disabled={disabled || busy || context?.valid === false}\n      // 倒计时每秒都在变，不用 aria-live 逐秒播报；读屏聚焦时会念出当前文字\n      onClick={() => void send()}\n      className={cn(\n        variant === \"inline\"\n          ? // 条内：撑满条高、左侧一道竖线隔开号码，文字用主题色，像一段可点的链接\n            \"-mr-3 flex h-full shrink-0 items-center border-l border-edge px-3 text-primary tabular-nums transition-colors outline-none hover:bg-muted focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:ring-inset disabled:cursor-not-allowed disabled:text-muted-foreground disabled:hover:bg-transparent max-md:text-sm @max-[20rem]/phone-input:px-2.5\"\n          : cn(buttonVariants({ variant: \"outline\", size }), \"tabular-nums\"),\n        className,\n      )}\n      {...props}\n    >\n      <span className=\"grid justify-items-center\">\n        {/* 用位置当 key：调用方可能把两种状态写成同一句文案（如 send 与 resend 都是「获取验证码」） */}\n        {sizers.map((sizer, index) => (\n          <span\n            key={index}\n            aria-hidden=\"true\"\n            className=\"invisible col-start-1 row-start-1 whitespace-nowrap\"\n          >\n            {sizer}\n          </span>\n        ))}\n        <span className=\"col-start-1 row-start-1 whitespace-nowrap\">\n          {text}\n        </span>\n      </span>\n    </button>\n  )\n}\n\nexport {\n  PhoneInputContext,\n  PhoneInputSendButton,\n  phoneInputControlVariants,\n  useSendCode,\n}\n",
      "type": "registry:ui",
      "target": "@ui/phone-input/phone-input-parts.tsx"
    },
    {
      "path": "src/registry/ui/phone-input/types.ts",
      "content": "import type * as React from \"react\"\n\nimport type { PhoneInputLabels } from \"./config\"\n\ntype PhoneInputSize = \"sm\" | \"default\" | \"lg\"\n\ntype PhoneInputProps = Omit<\n  React.ComponentProps<\"input\">,\n  \"value\" | \"defaultValue\" | \"onChange\" | \"type\" | \"size\" | \"children\"\n> & {\n  /** 手机号，11 位纯数字字符串（不带空格、区号）；空串表示没填。受控时传入 */\n  value?: string\n  /**\n   * 非受控时的初始值\n   * @default \"\"\n   */\n  defaultValue?: string\n  /** 号码变化时回调，参数是纯数字字符串 */\n  onValueChange?: (value: string) => void\n  /** 写在条内左侧的标签，自动与输入框关联；传入后号码靠右 */\n  label?: React.ReactNode\n  /**\n   * 档位：高度 28 / 32 / 36px，触屏设备上 36 / 40 / 44px\n   * @default \"default\"\n   */\n  size?: PhoneInputSize\n  /** 条内右侧的附加内容，通常放 `PhoneInputSendButton`（获取验证码） */\n  action?: React.ReactNode\n  /**\n   * 错误提示：显示在条下方，条变红。不传时组件自己校验，失焦后位数不够或号段不对会提示\n   */\n  error?: React.ReactNode\n  labels?: Partial<PhoneInputLabels>\n}\n\ntype PhoneInputSendButtonProps = Omit<\n  React.ComponentProps<\"button\">,\n  \"onClick\" | \"children\"\n> & {\n  /**\n   * 点击后执行的发送动作，返回 Promise 时等它完成再开始倒计时；抛错时不进入倒计时，可以马上重试\n   */\n  onSend: () => void | Promise<void>\n  /**\n   * 冷却时间（秒）\n   * @default 60\n   */\n  cooldown?: number\n  /**\n   * `inline` 放在手机号方条里（左侧一道竖线、主题色文字）；`button` 单独使用，和按钮同一外观\n   * @default \"inline\"\n   */\n  variant?: \"inline\" | \"button\"\n  /**\n   * `button` 外观的高度档位，与按钮一致：28 / 32 / 36px\n   * @default \"default\"\n   */\n  size?: \"sm\" | \"default\" | \"lg\"\n  labels?: Partial<PhoneInputLabels>\n}\n\nexport type { PhoneInputProps, PhoneInputSendButtonProps, PhoneInputSize }\n",
      "type": "registry:ui",
      "target": "@ui/phone-input/types.ts"
    },
    {
      "path": "src/registry/ui/phone-input/utils.ts",
      "content": "import { PHONE_LENGTH } from \"./config\"\n\n/** 全角数字转半角，其余非数字字符丢掉 */\nfunction digitsOf(text: string) {\n  let out = \"\"\n  for (const ch of text) {\n    const code = ch.charCodeAt(0)\n    if (code >= 0xff10 && code <= 0xff19)\n      out += String.fromCharCode(code - 0xfee0)\n    else if (ch >= \"0\" && ch <= \"9\") out += ch\n  }\n  return out\n}\n\n/**\n * 去掉国际区号：「+86 138…」「0086 138…」「86138…」都剩下 11 位。\n * 只在数字超过 11 位时才剥，免得把正在输入的号码误删。\n */\nfunction stripCountryCode(digits: string) {\n  if (digits.length > PHONE_LENGTH && digits.startsWith(\"0086\"))\n    return digits.slice(4)\n  if (digits.length > PHONE_LENGTH && digits.startsWith(\"86\"))\n    return digits.slice(2)\n  return digits\n}\n\n/** 11 位数字按 3-4-4 加空格：`13800138000` → `138 0013 8000`；不足 11 位时按已有位数分 */\nfunction formatPhone(digits: string) {\n  const d = digits.slice(0, PHONE_LENGTH)\n  if (d.length <= 3) return d\n  if (d.length <= 7) return `${d.slice(0, 3)} ${d.slice(3)}`\n  return `${d.slice(0, 3)} ${d.slice(3, 7)} ${d.slice(7)}`\n}\n\n/** 号码中间四位打码：`13800138000` → `138 **** 8000`；不是 11 位时原样返回 */\nfunction maskPhone(digits: string, mask = \"****\") {\n  const d = digitsOf(digits)\n  if (d.length !== PHONE_LENGTH) return digits\n  return `${d.slice(0, 3)} ${mask} ${d.slice(7)}`\n}\n\n/** 是否是有效的中国大陆手机号：11 位，以 13–19 开头 */\nfunction isValidPhone(digits: string) {\n  return /^1[3-9]\\d{9}$/.test(digits)\n}\n\n/**\n * 整理一次输入：返回格式化后的文本、换算后的光标位置和纯数字值。\n * 光标按「它前面有几位数字」换算，所以增删空格、粘贴带横线的号码时光标都落在同一个数字后面。\n */\nfunction sanitizePhone(raw: string, caret: number) {\n  const before = digitsOf(raw.slice(0, caret)).length\n  const all = digitsOf(raw)\n  const stripped = stripCountryCode(all)\n  const removed = all.length - stripped.length\n  const digits = stripped.slice(0, PHONE_LENGTH)\n  const text = formatPhone(digits)\n  const want = Math.min(Math.max(before - removed, 0), digits.length)\n  let position = 0\n  let seen = 0\n  while (position < text.length && seen < want) {\n    if (text[position] !== \" \") seen++\n    position++\n  }\n  return { text, caret: position, digits }\n}\n\nexport { digitsOf, formatPhone, isValidPhone, maskPhone, sanitizePhone }\n",
      "type": "registry:ui",
      "target": "@ui/phone-input/utils.ts"
    },
    {
      "path": "src/registry/ui/phone-input/config.ts",
      "content": "/** 用户可见文案。`{seconds}` 会被替换成剩余秒数。 */\nconst DEFAULT_LABELS = {\n  /** 输入框的提示文字 */\n  placeholder: \"请输入手机号\",\n  /** 位数不够或号段不对时的提示，失焦后显示 */\n  invalid: \"请输入正确的手机号\",\n  /** 必填却没填时的提示 */\n  required: \"请输入手机号\",\n  /** 发送验证码按钮：初始 */\n  send: \"获取验证码\",\n  /** 发送中 */\n  sending: \"发送中…\",\n  /** 倒计时中 */\n  countdown: \"{seconds} 秒后重发\",\n  /** 倒计时结束 */\n  resend: \"重新获取\",\n}\n\ntype PhoneInputLabels = typeof DEFAULT_LABELS\n\n/** 中国大陆手机号的位数 */\nconst PHONE_LENGTH = 11\n\n/** 发送验证码后的默认冷却时间（秒） */\nconst DEFAULT_COOLDOWN = 60\n\nexport { DEFAULT_COOLDOWN, DEFAULT_LABELS, PHONE_LENGTH }\nexport type { PhoneInputLabels }\n",
      "type": "registry:ui",
      "target": "@ui/phone-input/config.ts"
    },
    {
      "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"
    },
    {
      "path": "src/registry/hooks/use-caret-restore.ts",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\n/** 格式化提交后恢复光标；即使文本未变，也用新请求触发恢复。 */\nfunction useCaretRestore(inputRef: React.RefObject<HTMLInputElement | null>) {\n  const [caret, setCaret] = React.useState<{ position: number } | null>(null)\n  React.useLayoutEffect(() => {\n    const input = inputRef.current\n    if (caret && input && document.activeElement === input)\n      input.setSelectionRange(caret.position, caret.position)\n  }, [caret, inputRef])\n  return setCaret\n}\n\nexport { useCaretRestore }\n",
      "type": "registry:ui",
      "target": "hooks/use-caret-restore.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"
}
