{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "attachment",
  "title": "Attachment",
  "description": "shadcn/ui(new-york-v4) attachment의 유니버설(웹+네이티브 한 벌 소스) 포팅. 포팅 판단(원본 대비 차이·이유): 재사용한다(원본 의존과 동일 구성). 원본 기본 size \"icon-xs\"는 UniKit UI",
  "dependencies": [
    "@unikit-bridge/ui",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/attachment.tsx",
      "content": "// Attachment — shadcn/ui（new-york-v4） attachment.tsx 유니버설 포팅 （사이클 40, T-3d）.\n// 원본: shadcn-ref/ui/attachment.tsx\n//\n// 포팅 판단（원본 대비 차이이유）:\n// - div/span → 브리지 View/Text. AttachmentAction은 기존 Button（\"./button\"）을\n//   재사용한다（원본 의존과 동일 구성）. 원본 기본 size \"icon-xs\"는 사이클 71에\n//   UniKit UI Button 스펙에 신설됐다 — 포팅은 기본값을 물리지 않아 소비자가\n//   size=\"icon-xs\"를 지정하는 계약 그대로 둔다.\n// - data-state/data-size/data-orientation + group-data- 셀렉터는 네이티브에\n//   data 속성이 없어 AttachmentContext { size, orientation, state, hasContent,\n//   hasMedia } + 조회 테이블로 재현 — 자식（Media/Content/Title/）이 context를\n//   읽어 스스로 크기색위치를 적용한다（avatar.tsx 패턴）.\n// - 원본 `has-data-［slot=attachment-content］:px-2.5` 조건부 패딩은 자식\n//   \"마운트 등록\"（reportContentMounted/reportMediaMounted）으로 근사한다.\n// - AttachmentGroup은 div overflow-x-auto 대신 브리지 ScrollView（horizontal）를\n//   직접 소비 — 네이티브에서 div overflow 스크롤이 동작하지 않기 때문.\n//   snap/scroll-px/overscroll은 웹 전용 → web: 접두. scroll-fade-x\n//   scrollbar-none은 커스텀 유틸이라 제거.\n// - shimmer（업로드 중 광택 애니메이션）`*:data-［slot=spinner］:size-6!`\n//   ［&_svg:not（）］:size-* 셀렉터는 정의셀렉터 부재로 제거 — 진행 표시는\n//   AttachmentMedia의 image 변형 opacity로 근사.\n// - asChild（radix Slot） 제거 — AttachmentTrigger는 Pressable 렌더 고정,\n//   type 속성은 브리지 계약에 없어 제거.\n// - truncatefocus-withinhover는 웹 전용 → web: 접두（네이티브은 전체 표시）.\n// - attachment-media의 전경색（error 글리프 등）은 이중 지정 원칙에 따라\n//   소비자 Text에 명시 — 배경상태색만 context가 적용한다.\n// - 원본 has-［>a,>button］:hover 배경은 has 임의값 셀렉터라 hover:bg-muted/50로 근사（T-4 감사 반영）.\nimport {\n  createContext,\n  forwardRef,\n  useContext,\n  useEffect,\n  useMemo,\n  useState,\n  type ReactNode,\n} from \"react\";\nimport {\n  Pressable,\n  ScrollView,\n  Text,\n  View,\n  type BridgePressableRef,\n  type BridgeScrollViewRef,\n  type BridgeTextRef,\n  type BridgeViewRef,\n  type PressableProps,\n  type ScrollViewProps,\n  type TextProps,\n  type ViewProps,\n} from \"@unikit-bridge/ui/primitives\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Button, type ButtonProps } from \"./button\";\n\nexport type AttachmentState =\n  | \"idle\"\n  | \"uploading\"\n  | \"processing\"\n  | \"error\"\n  | \"done\";\nexport type AttachmentSize = \"default\" | \"sm\" | \"xs\";\nexport type AttachmentOrientation = \"horizontal\" | \"vertical\";\n\n// Attachment가 제공하는 상태 — group-data 셀렉터 재현의 단일 출처.\ntype AttachmentContextValue = {\n  size: AttachmentSize;\n  orientation: AttachmentOrientation;\n  state: AttachmentState;\n  hasContent: boolean;\n  hasMedia: boolean;\n  reportContentMounted: (mounted: boolean) => void;\n  reportMediaMounted: (mounted: boolean) => void;\n};\n\nconst AttachmentContext = createContext<AttachmentContextValue | null>(null);\n\n// 콘텐츠미디어 있을 때의 루트 패딩（원본 has-data 조건부 토큰의 조회 테이블）.\nconst ATTACHMENT_PADDING_CONTENT: Record<AttachmentSize, string> = {\n  default: \"px-2.5 py-2\",\n  sm: \"px-2 py-1.5\",\n  xs: \"px-1.5 py-1\",\n};\nconst ATTACHMENT_PADDING_MEDIA: Record<AttachmentSize, string> = {\n  default: \"p-2\",\n  sm: \"p-1.5\",\n  xs: \"p-1\",\n};\n\n// 미디어 칸 크기（원본 group-data-［size=］:w-* 치환）.\nconst ATTACHMENT_MEDIA_SIZE: Record<AttachmentSize, string> = {\n  default: \"w-10\",\n  sm: \"w-8\",\n  xs: \"w-7\",\n};\n\n// 제목 글자 크기（원본 루트 text-sm/text-xs 상속 치환 — Title이 스스로 적용）.\nconst ATTACHMENT_TITLE_TEXT: Record<AttachmentSize, string> = {\n  default: \"text-sm\",\n  sm: \"text-xs\",\n  xs: \"text-xs\",\n};\n\n// ---------------------------------------------------------------------------\n// Attachment — 첨부 카드 루트. 상태 제공자역을 겸한다.\n// ---------------------------------------------------------------------------\nconst attachmentVariants = cva(\n  \"relative flex w-fit max-w-full min-w-0 shrink-0 flex-wrap rounded-xl border bg-card web:snap-start web:transition-colors web:hover:bg-muted/50 web:focus-within:ring-1 web:focus-within:ring-ring/50\",\n  {\n    variants: {\n      size: {\n        default: \"gap-2\",\n        sm: \"gap-2.5\",\n        xs: \"gap-1.5 rounded-lg\",\n      },\n      orientation: {\n        horizontal: \"min-w-40 items-center\",\n        vertical: \"w-24 flex-col\",\n      },\n      state: {\n        idle: \"border-dashed\",\n        uploading: \"\",\n        processing: \"\",\n        error: \"border-destructive/30\",\n        done: \"\",\n      },\n    },\n    defaultVariants: {\n      size: \"default\",\n      orientation: \"horizontal\",\n      state: \"done\",\n    },\n  },\n);\n\nexport type AttachmentProps = ViewProps &\n  VariantProps<typeof attachmentVariants> & {\n    state?: AttachmentState;\n    children?: ReactNode;\n  };\n\nconst Attachment = forwardRef<BridgeViewRef, AttachmentProps>(\n  (\n    { className, size, orientation, state, ...props },\n    ref,\n  ) => {\n    const [hasContent, setHasContent] = useState(false);\n    const [hasMedia, setHasMedia] = useState(false);\n    // VariantProps는 null을 허용하므로 기본값 병합 후 context에 제공한다.\n    const resolvedSize = size ?? \"default\";\n    const resolvedOrientation = orientation ?? \"horizontal\";\n    const resolvedState = state ?? \"done\";\n    const contextValue = useMemo<AttachmentContextValue>(\n      () => ({\n        size: resolvedSize,\n        orientation: resolvedOrientation,\n        state: resolvedState,\n        hasContent,\n        hasMedia,\n        // setState 설정자는 참조가 안정적이라 그대로 노출해도 안전하다.\n        reportContentMounted: setHasContent,\n        reportMediaMounted: setHasMedia,\n      }),\n      [resolvedSize, resolvedOrientation, resolvedState, hasContent, hasMedia],\n    );\n    return (\n      <AttachmentContext.Provider value={contextValue}>\n        <View\n          ref={ref}\n          className={cn(\n            attachmentVariants({\n              size: resolvedSize,\n              orientation: resolvedOrientation,\n              state: resolvedState,\n            }),\n            // 원본 has-data-[slot=attachment-content]:w-30 치환 — 콘텐츠가\n            // 마운트되면 세로형 루트가 w-24에서 w-30으로 확장한다(자식 폭 초과 방지).\n            resolvedOrientation === \"vertical\" && hasContent && \"w-30\",\n            hasContent && ATTACHMENT_PADDING_CONTENT[resolvedSize],\n            hasMedia && ATTACHMENT_PADDING_MEDIA[resolvedSize],\n            className,\n          )}\n          {...props}\n        />\n      </AttachmentContext.Provider>\n    );\n  },\n);\nAttachment.displayName = \"Attachment\";\n\n// ---------------------------------------------------------------------------\n// AttachmentMedia — 아이콘/이미지 칸. 크기상태색은 context 연동.\n// 글리프는 children Text（색 명시）, 이미지는 children Image（aspect-square\n// w-full 클래스 권장 — 원본 *:［img］: 정렬 셀렉터의 안내 치환）.\n// ---------------------------------------------------------------------------\nconst attachmentMediaVariants = cva(\n  \"relative flex aspect-square shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted\",\n  {\n    variants: {\n      variant: {\n        icon: \"\",\n        image: \"opacity-60\",\n      },\n    },\n    defaultVariants: {\n      variant: \"icon\",\n    },\n  },\n);\n\nexport type AttachmentMediaProps = ViewProps &\n  VariantProps<typeof attachmentMediaVariants> & {\n    children?: ReactNode;\n  };\n\nconst AttachmentMedia = forwardRef<BridgeViewRef, AttachmentMediaProps>(\n  ({ className, variant = \"icon\", children, ...props }, ref) => {\n    const attachment = useContext(AttachmentContext);\n    const report = attachment?.reportMediaMounted;\n    // 마운트 등록/해제 — 루트 패딩 판단의 근거（provider는 안정적）.\n    useEffect(() => {\n      report?.(true);\n      return () => report?.(false);\n    }, [report]);\n    const size = attachment?.size ?? \"default\";\n    const orientation = attachment?.orientation ?? \"horizontal\";\n    const state = attachment?.state ?? \"done\";\n    // 이미지는 완료（idle 포함） 상태에서만 완전 불투명（원본 opacity 토큰）.\n    const imageReady =\n      variant === \"image\" && (state === \"done\" || state === \"idle\");\n    return (\n      <View\n        ref={ref}\n        className={cn(\n          attachmentMediaVariants({ variant }),\n          ATTACHMENT_MEDIA_SIZE[size],\n          orientation === \"vertical\" && \"w-full\",\n          size === \"xs\" && \"rounded-md\",\n          state === \"error\" && \"bg-destructive/10\",\n          imageReady && \"opacity-100\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </View>\n    );\n  },\n);\nAttachmentMedia.displayName = \"AttachmentMedia\";\n\n// ---------------------------------------------------------------------------\n// AttachmentContent — 제목설명 본문 영역.\n// ---------------------------------------------------------------------------\nexport type AttachmentContentProps = ViewProps;\n\nconst AttachmentContent = forwardRef<BridgeViewRef, AttachmentContentProps>(\n  ({ className, ...props }, ref) => {\n    const attachment = useContext(AttachmentContext);\n    const report = attachment?.reportContentMounted;\n    // 마운트 등록/해제 — 루트 패딩 판단의 근거（provider는 안정적）.\n    useEffect(() => {\n      report?.(true);\n      return () => report?.(false);\n    }, [report]);\n    return (\n      <View\n        ref={ref}\n        className={cn(\n          \"max-w-full min-w-0 flex-1 leading-tight\",\n          attachment?.orientation === \"vertical\" && \"px-1\",\n          className,\n        )}\n        {...props}\n      />\n    );\n  },\n);\nAttachmentContent.displayName = \"AttachmentContent\";\n\n// ---------------------------------------------------------------------------\n// AttachmentTitle — 파일명 등 제목. 크기는 루트 size 연동.\n// ---------------------------------------------------------------------------\nexport type AttachmentTitleProps = TextProps & {\n  children?: ReactNode;\n};\n\nconst AttachmentTitle = forwardRef<BridgeTextRef, AttachmentTitleProps>(\n  ({ className, children, ...props }, ref) => {\n    const attachment = useContext(AttachmentContext);\n    return (\n      <Text\n        ref={ref}\n        className={cn(\n          \"max-w-full min-w-0 font-medium text-card-foreground web:truncate\",\n          attachment && ATTACHMENT_TITLE_TEXT[attachment.size],\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </Text>\n    );\n  },\n);\nAttachmentTitle.displayName = \"AttachmentTitle\";\n\n// ---------------------------------------------------------------------------\n// AttachmentDescription — 보조 설명（용량진행 상태 등）. error면 붉은 계열.\n// ---------------------------------------------------------------------------\nexport type AttachmentDescriptionProps = TextProps & {\n  children?: ReactNode;\n};\n\nconst AttachmentDescription = forwardRef<BridgeTextRef, AttachmentDescriptionProps>(\n  ({ className, children, ...props }, ref) => {\n    const attachment = useContext(AttachmentContext);\n    return (\n      <Text\n        ref={ref}\n        className={cn(\n          \"mt-0.5 min-w-0 max-w-full text-xs text-muted-foreground web:truncate\",\n          attachment?.state === \"error\" && \"text-destructive/80\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </Text>\n    );\n  },\n);\nAttachmentDescription.displayName = \"AttachmentDescription\";\n\n// ---------------------------------------------------------------------------\n// AttachmentActions — 우측（세로형은 우상단） 액션 영역.\n// ---------------------------------------------------------------------------\nexport type AttachmentActionsProps = ViewProps;\n\nconst AttachmentActions = forwardRef<BridgeViewRef, AttachmentActionsProps>(\n  ({ className, ...props }, ref) => {\n    const attachment = useContext(AttachmentContext);\n    return (\n      <View\n        ref={ref}\n        className={cn(\n          \"relative z-20 flex flex-row shrink-0 items-center\",\n          attachment?.orientation === \"vertical\" &&\n            \"absolute top-3 right-3 gap-1\",\n          className,\n        )}\n        {...props}\n      />\n    );\n  },\n);\nAttachmentActions.displayName = \"AttachmentActions\";\n\n// ---------------------------------------------------------------------------\n// AttachmentAction — 개별 액션 버튼（기존 Button 재사용）.\n// ---------------------------------------------------------------------------\nexport type AttachmentActionProps = ButtonProps;\n\nconst AttachmentAction = forwardRef<BridgePressableRef, AttachmentActionProps>(\n  ({ className, variant, ...props }, ref) => (\n    <Button\n      ref={ref}\n      variant={variant ?? \"ghost\"}\n      className={cn(className)}\n      {...props}\n    />\n  ),\n);\nAttachmentAction.displayName = \"AttachmentAction\";\n\n// ---------------------------------------------------------------------------\n// AttachmentTrigger — 카드 전체를 눌러 열기/제거하는 투명 오버레이 버튼.\n// ---------------------------------------------------------------------------\nexport type AttachmentTriggerProps = PressableProps;\n\nconst AttachmentTrigger = forwardRef<BridgePressableRef, AttachmentTriggerProps>(\n  ({ className, ...props }, ref) => (\n    <Pressable\n      ref={ref}\n      accessibilityRole=\"button\"\n      className={cn(\"absolute inset-0 z-10 web:outline-none\", className)}\n      {...props}\n    />\n  ),\n);\nAttachmentTrigger.displayName = \"AttachmentTrigger\";\n\n// ---------------------------------------------------------------------------\n// AttachmentGroup — 첨부 가로 나열（브리지 ScrollView horizontal 소비）.\n// ---------------------------------------------------------------------------\nexport type AttachmentGroupProps = ScrollViewProps;\n\nconst AttachmentGroup = forwardRef<BridgeScrollViewRef, AttachmentGroupProps>(\n  ({ className, contentContainerClassName, ...props }, ref) => (\n    <ScrollView\n      ref={ref}\n      horizontal\n      className={cn(\n        \"web:snap-x web:snap-mandatory web:scroll-px-1 web:overscroll-x-contain\",\n        className,\n      )}\n      contentContainerClassName={cn(\"gap-3 py-1\", contentContainerClassName)}\n      {...props}\n    />\n  ),\n);\nAttachmentGroup.displayName = \"AttachmentGroup\";\n\nexport {\n  Attachment,\n  AttachmentGroup,\n  AttachmentMedia,\n  AttachmentContent,\n  AttachmentTitle,\n  AttachmentDescription,\n  AttachmentActions,\n  AttachmentAction,\n  AttachmentTrigger,\n};\n",
      "type": "registry:ui",
      "target": "components/ui/attachment.tsx"
    }
  ],
  "type": "registry:ui"
}
