{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "form",
  "title": "Form",
  "description": "shadcn/ui(new-york-v4) form의 유니버설(웹+네이티브 한 벌 소스) 포팅. name·formItemId·formDescriptionId·formMessageId·error)은 그대로 유지하고 Radix Slot·<div>/<p> DOM을 브리지 프리미티브로 치환한다. ※ form 연동용(선택) peer 의존.",
  "dependencies": [
    "@unikit-bridge/ui"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/form.tsx",
      "content": "// Form — shadcn/ui（new-york-v4） form.tsx 유니버설 포팅 （사이클 40, T-2c）.\n// 원본: shadcn-ref/ui/form.tsx — react-hook-form 연동（useFormField 컨텍스트 —\n// nameformItemIdformDescriptionIdformMessageIderror）은 그대로 유지하고\n// Radix Slot<div>/<p> DOM을 브리지 프리미티브로 치환한다.\n//\n// 포팅 판단（원본 대비 차이이유）:\n// - FormControl의 Radix Slot（단일 자식 요소에 props 병합） 제거 — 단일 자식（입력\n//   프리미티브）에 cloneElement로 접근성 연결 속성을 병합 주입한다（스프레드\n//   props）. 주입값: nativeID（=formItemId）accessibilityLabelledBy（=formLabelId）\n//   aria-invalid（오류 시 true）aria-describedby（설명+오류 조립）. 사용측 직접\n//   지정이 주입값보다 우선이다（원본 Slot 병합 순서와 동일）.\n// - 원본 label［htmlFor］ ↔ input［id］ 연결은 브리지 계약（label.tsx 머리주석의\n//   관용）에 따라 Label nativeID ↔ 입력 accessibilityLabelledBy 방향으로 치환\n//   — 이 연결에 필요한 formLabelId 키를 useFormField 반환에 추가했다.\n// - div/p → View/Text 평면화. 원본 FormItem의 \"grid gap-2\"는 grid가\n//   네이티브 스타일링에서 미보증이라 \"flex flex-col gap-2\"로 근사（세로 스택\n//   동일）. FormLabel의 data-error=true → text-destructive는 조건부 클래스로\n//   치환（네이티브에 data 속성 없음）.\n// - Form（FormProvider）FormField（Controller 래퍼）는 원본이 forwardRef가\n//   아니므로 함수형 그대로 유지한다. FormControl은 ref를 지원하지 않는다\n//   （원본은 Slot ref가 자식으로 흘렀다） — ref가 필요하면 자식 입력\n//   프리미티브에 직접 달 것.\n// - FormMessage는 원본 그대로 error.message（없으면 children）를 렌더하며\n//   body가 없으면 null을 반환한다.\n// - RN 한계: aria-invalidaria-describedby는 웹 전용 계약이다（types.ts —\n//   RN 입력창에는 미전달）. nativeIDaccessibilityLabelledBy는 양쪽에서\n//   전달돼 라벨 연결은 유지된다.\nimport {\n  cloneElement,\n  createContext,\n  forwardRef,\n  isValidElement,\n  useContext,\n  useId,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\nimport {\n  Controller,\n  FormProvider,\n  useFormContext,\n  useFormState,\n  type ControllerProps,\n  type FieldPath,\n  type FieldValues,\n} from \"react-hook-form\";\nimport {\n  Text,\n  View,\n  type BridgeTextRef,\n  type BridgeViewRef,\n  type TextProps,\n  type ViewProps,\n} from \"@unikit-bridge/ui/primitives\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Label, type LabelProps } from \"./label\";\n\nconst Form = FormProvider;\n\ntype FormFieldContextValue<\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n> = {\n  name: TName;\n};\n\nconst FormFieldContext = createContext<FormFieldContextValue>(\n  {} as FormFieldContextValue,\n);\n\nconst FormField = <\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n>({\n  ...props\n}: ControllerProps<TFieldValues, TName>) => {\n  return (\n    <FormFieldContext.Provider value={{ name: props.name }}>\n      <Controller {...props} />\n    </FormFieldContext.Provider>\n  );\n};\nFormField.displayName = \"FormField\";\n\nconst useFormField = () => {\n  const fieldContext = useContext(FormFieldContext);\n  const itemContext = useContext(FormItemContext);\n  const { getFieldState } = useFormContext();\n  const formState = useFormState({ name: fieldContext.name });\n  const fieldState = getFieldState(fieldContext.name, formState);\n\n  if (!fieldContext) {\n    throw new Error(\"useFormField는 <FormField> 안에서 사용해야 합니다.\");\n  }\n\n  const { id } = itemContext;\n\n  return {\n    id,\n    name: fieldContext.name,\n    formItemId: `${id}-form-item`,\n    formLabelId: `${id}-form-item-label`,\n    formDescriptionId: `${id}-form-item-description`,\n    formMessageId: `${id}-form-item-message`,\n    ...fieldState,\n  };\n};\n\ntype FormItemContextValue = {\n  id: string;\n};\n\nconst FormItemContext = createContext<FormItemContextValue>(\n  {} as FormItemContextValue,\n);\n\nexport type FormItemProps = ViewProps;\n\nconst FormItem = forwardRef<BridgeViewRef, FormItemProps>(\n  ({ className, ...props }, ref) => {\n    const id = useId();\n    return (\n      <FormItemContext.Provider value={{ id }}>\n        <View\n          ref={ref}\n          className={cn(\"flex flex-col gap-2\", className)}\n          {...props}\n        />\n      </FormItemContext.Provider>\n    );\n  },\n);\nFormItem.displayName = \"FormItem\";\n\nexport type FormLabelProps = LabelProps;\n\nconst FormLabel = forwardRef<BridgeTextRef, FormLabelProps>(\n  ({ className, ...props }, ref) => {\n    const { error, formLabelId } = useFormField();\n    return (\n      <Label\n        ref={ref}\n        nativeID={formLabelId}\n        // 원본 data-error=true → text-destructive 조건부 치환\n        className={cn(error && \"text-destructive\", className)}\n        {...props}\n      />\n    );\n  },\n);\nFormLabel.displayName = \"FormLabel\";\n\n// 단일 자식 입력 프리미티브에서 병합 주입에 쓰는 계약 속성（나머지는 그대로 스프레드）\ntype ControlChildProps = {\n  className?: string;\n  nativeID?: string;\n  accessibilityLabelledBy?: string;\n  \"aria-invalid\"?: boolean;\n  \"aria-describedby\"?: string;\n};\n\nexport type FormControlProps = {\n  /** 단일 입력 프리미티브（InputTextarea 등） — 접근성 연결 속성이 병합 주입된다 */\n  children?: ReactNode;\n  className?: string;\n  // 참고: 인덱스 시그니처（［key: string］: unknown）를 열면 forwardRef의\n  // Omit<P, \"ref\"> 처리에서 명시적 프로퍼티까지 unknown으로 붕괴해 타입을 닫았다.\n  // 여분 props의 자식 전달（스프레드）은 런타임에 그대로 유지된다 — 속성은 자식\n  // 프리미티브에 직접 지정할 것.\n};\n\nconst FormControl = forwardRef<BridgeViewRef, FormControlProps>(\n  ({ className, children, ...props }, _ref) => {\n    const { error, formItemId, formLabelId, formDescriptionId, formMessageId } =\n      useFormField();\n\n    // Slot 대리 — 감쌀 자식 엘리먼트가 없으면 그대로 반환한다\n    if (!isValidElement(children)) {\n      return (children ?? null) as ReactNode;\n    }\n    const child = children as ReactElement<ControlChildProps>;\n    const injected: ControlChildProps = {\n      nativeID: formItemId,\n      accessibilityLabelledBy: formLabelId,\n      \"aria-invalid\": error ? true : undefined,\n      \"aria-describedby\": error\n        ? `${formDescriptionId} ${formMessageId}`\n        : formDescriptionId,\n      className: cn(child.props.className, className),\n    };\n    // 사용측 직접 지정（props）이 주입값보다 우선 — 원본 Slot 병합 순서\n    return cloneElement(child, { ...injected, ...props } as ControlChildProps);\n  },\n);\nFormControl.displayName = \"FormControl\";\n\nexport type FormDescriptionProps = TextProps;\n\nconst FormDescription = forwardRef<BridgeTextRef, FormDescriptionProps>(\n  ({ className, ...props }, ref) => {\n    const { formDescriptionId } = useFormField();\n    return (\n      <Text\n        ref={ref}\n        nativeID={formDescriptionId}\n        className={cn(\"font-sans text-sm text-muted-foreground\", className)}\n        {...props}\n      />\n    );\n  },\n);\nFormDescription.displayName = \"FormDescription\";\n\nexport type FormMessageProps = TextProps;\n\nconst FormMessage = forwardRef<BridgeTextRef, FormMessageProps>(\n  ({ className, children, ...props }, ref) => {\n    const { error, formMessageId } = useFormField();\n    const body = error ? String(error?.message ?? \"\") : children;\n\n    if (!body) {\n      return null;\n    }\n\n    return (\n      <Text\n        ref={ref}\n        nativeID={formMessageId}\n        className={cn(\"font-sans text-sm text-destructive\", className)}\n        {...props}\n      >\n        {body}\n      </Text>\n    );\n  },\n);\nFormMessage.displayName = \"FormMessage\";\n\nexport {\n  useFormField,\n  Form,\n  FormItem,\n  FormLabel,\n  FormControl,\n  FormDescription,\n  FormMessage,\n  FormField,\n};\n",
      "type": "registry:ui",
      "target": "components/ui/form.tsx"
    }
  ],
  "type": "registry:ui"
}
