{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "resizable",
  "title": "Resizable",
  "description": "shadcn/ui(new-york-v4) resizable의 유니버설(웹+네이티브 한 벌 소스) 포팅. 브리지 useDrag(웹 PointerEvent·네이티브 PanResponder 동일 계약) 기반 2패널 근사로 직접 구현한다. PanelGroup/Panel/Handle API명은 원본을 유지한다.",
  "dependencies": [
    "@unikit-bridge/ui"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/resizable.tsx",
      "content": "// Resizable — shadcn/ui（new-york-v4） resizable.tsx 유니버설 포팅 （사이클 40, T-2b）.\n// 원본: shadcn-ref/ui/resizable.tsx — react-resizable-panels 엔진을 제거하고\n// 브리지 useDrag（웹 PointerEvent네이티브 PanResponder 동일 계약） 기반 2패널\n// 근사로 직접 구현한다. PanelGroup/Panel/Handle API명은 원본을 유지한다.\n//\n// 포팅 판단（원본 대비 차이）:\n// - 드래그: 핸들에서 useDrag로 제스처를 받고, 그룹이 measureInWindow로 컨테이너\n//   폭（수직은 높이）을 캐시해 dx（수직은 dy）를 백분율로 환산, 첫 패널의\n//   flex-basis를 동적 % （BridgeStyle）로 조정한다 — progress.tsx 관행.\n// - 3패널 이상은 부분 근사: 각 핸들은 자기 왼쪽（수직은 위） 패널의 basis만\n//   조정하고 마지막 패널은 남은 폭을 차지한다（flex-1）. 미지원: 비율 저장\n//   （autoSaveId）중첩 그룹패널 collapse 등 react-resizable-panels 전용 기능.\n// - 제약: Panel의 defaultSize/minSize/maxSize（%）를 그룹이 children에서 읽어\n//   클램프에 반영한다. 기본 minSize 10（원본 라이브러리 기본）maxSize 90\n//   （마지막 패널 최소 폭 확보 근사）.\n// - 핸들은 Pressable이 아닌 View에 dragHandlers를 펼친다（드래그 전용 — 클릭\n//   시맨틱 없음）. role=\"separator\"만 부여하고, 원본의 focus-visible 링키보드\n//   리사이즈（화살표）는 포커서블 계약（tabIndex）이 없어 미지원 — 로드맵 항목.\n// - 원본 핸들의 after: 가상요소 히트 영역（웹 전용） 대신 핸들 자체를 w-1（수직\n//   h-1）로 잡고 시각 바（w-px）를 내부 View로 분리해 양 플랫폼 동일 히트 영역.\n// - GripVerticalIcon은 브리지 primitives 경유（lucide）. 수직 방향（horizontal\n//   false）은 rotate-90로 원본과 같이 가로 그립으로 근사.\nimport {\n  Children,\n  cloneElement,\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useEffect,\n  isValidElement,\n  useMemo,\n  useRef,\n  useState,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\nimport {\n  GripVerticalIcon,\n  View,\n  measureInWindow,\n  useDrag,\n  type BridgeDragEvent,\n  type BridgeViewRef,\n  type ViewProps,\n} from \"@unikit-bridge/ui/primitives\";\n\nimport { cn } from \"@/lib/utils\";\n\n// 패널 기본 제약（%） — 원본 라이브러리 기본 minSize 10 준수.\nconst DEFAULT_MIN_SIZE = 10;\nconst DEFAULT_MAX_SIZE = 90;\n\ntype ResizableContextValue = {\n  direction: \"horizontal\" | \"vertical\";\n  /** 각 패널의 flex-basis %（마지막 패널은 자동 분배를 뜻하는 null） */\n  sizes: (number | null)[];\n  beginResize: (index: number) => void;\n  moveResize: (index: number, event: BridgeDragEvent) => void;\n};\n\nconst ResizableContext = createContext<ResizableContextValue | null>(null);\n\nfunction useResizableContext(component: string): ResizableContextValue {\n  const ctx = useContext(ResizableContext);\n  if (!ctx) {\n    throw new Error(`<${component}>는 <ResizablePanelGroup> 안에서 사용해야 합니다.`);\n  }\n  return ctx;\n}\n\n// 패널 제약 — 그룹이 children 순회 시 수집（렌더 시점 계산, 별도 등록 없음）.\ntype PanelConstraint = {\n  defaultSize?: number;\n  minSize: number;\n  maxSize: number;\n};\n\nexport type ResizablePanelGroupProps = ViewProps & {\n  /** 분할 방향（기본 수평） */\n  direction?: \"horizontal\" | \"vertical\";\n  children?: ReactNode;\n};\n\nconst ResizablePanelGroup = forwardRef<BridgeViewRef, ResizablePanelGroupProps>(\n  ({ className, direction = \"horizontal\", children, ...props }, ref) => {\n    // Panel/Handle 순회 — 각각에 순서 index를 주입하고 패널 제약을 수집한다.\n    const { decorated, constraints } = useMemo(() => {\n      const panelConstraints: PanelConstraint[] = [];\n      let panelCount = 0;\n      const items = Children.toArray(children).map((child) => {\n        if (!isValidElement(child)) return child;\n        const element = child as ReactElement;\n        if (element.type === ResizablePanel) {\n          const panel = element.props as ResizablePanelProps;\n          panelConstraints.push({\n            defaultSize: panel.defaultSize,\n            minSize: panel.minSize ?? DEFAULT_MIN_SIZE,\n            maxSize: panel.maxSize ?? DEFAULT_MAX_SIZE,\n          });\n          const indexed = cloneElement(\n            element as ReactElement<ResizablePanelProps>,\n            { index: panelCount },\n          );\n          panelCount += 1;\n          return indexed;\n        }\n        if (element.type === ResizableHandle) {\n          // 핸들은 자기 왼쪽（수직은 위） 패널 — 직전 패널을 조정 대상으로 받는다.\n          return cloneElement(\n            element as ReactElement<ResizableHandleProps>,\n            { index: panelCount - 1 },\n          );\n        }\n        return child;\n      });\n      return { decorated: items, constraints: panelConstraints };\n    }, [children]);\n\n    // 비율 상태 — 마지막 패널은 basis 없이 잔여 폭（flex-1）을 뜻하는 null.\n    const [sizes, setSizes] = useState<(number | null)[]>(() => {\n      const share = Math.floor(100 / Math.max(1, constraints.length));\n      return constraints.map((constraint, index) =>\n        index === constraints.length - 1\n          ? null\n          : (constraint.defaultSize ?? share),\n      );\n    });\n\n    // 드래그 세션 — 컨테이너 축 크기（캐시）와 제스처 시작 시점의 basis.\n    const containerRef = useRef<BridgeViewRef>(null);\n    const axisRef = useRef(0);\n    const startRef = useRef(0);\n    const sizesRef = useRef(sizes);\n    sizesRef.current = sizes;\n    const measureContainer = useCallback(() => {\n      measureInWindow(containerRef.current, (_x, _y, width, height) => {\n        axisRef.current = direction === \"horizontal\" ? width : height;\n      });\n    }, [direction]);\n    useEffect(measureContainer, [measureContainer]);\n\n    const beginResize = useCallback(\n      (index: number) => {\n        measureContainer();\n        startRef.current = sizesRef.current[index] ?? 0;\n      },\n      [measureContainer],\n    );\n    const moveResize = useCallback(\n      (index: number, event: BridgeDragEvent) => {\n        const axis = axisRef.current;\n        if (index < 0 || axis <= 0) return;\n        const delta = direction === \"horizontal\" ? event.dx : event.dy;\n        const { minSize, maxSize } = constraints[index]!;\n        const next = Math.min(\n          maxSize,\n          Math.max(minSize, startRef.current + (delta / axis) * 100),\n        );\n        setSizes((prev) =>\n          prev.map((size, i) => (i === index ? next : size)),\n        );\n      },\n      [direction, constraints],\n    );\n\n    // 사용측 ref와 내부 측정 ref 병합 — 하나의 DOM/RN 요소를 공유한다.\n    const combinedRef = useCallback(\n      (node: BridgeViewRef | null) => {\n        containerRef.current = node;\n        if (typeof ref === \"function\") ref(node);\n        else if (ref) (ref as { current: BridgeViewRef | null }).current = node;\n      },\n      [ref],\n    );\n\n    const contextValue = useMemo(\n      () => ({ direction, sizes, beginResize, moveResize }),\n      [direction, sizes, beginResize, moveResize],\n    );\n\n    return (\n      <ResizableContext.Provider value={contextValue}>\n        <View\n          ref={combinedRef}\n          className={cn(\n            \"flex h-full w-full\",\n            // 원본은 브라우저 flex-direction:row 기본에 의존(vertical만 flex-col 지정) —\n            // 브리지 View는 양 플랫폼 모두 flex-col 기본이라 수평 flex-row를 명시해야 한다.\n            direction === \"vertical\" ? \"flex-col\" : \"flex-row\",\n            className,\n          )}\n          {...props}\n        >\n          {decorated}\n        </View>\n      </ResizableContext.Provider>\n    );\n  },\n);\nResizablePanelGroup.displayName = \"ResizablePanelGroup\";\n\nexport type ResizablePanelProps = ViewProps & {\n  /** 초기 크기（%） — 미지정 시 균등 분배 */\n  defaultSize?: number;\n  /** 최소 크기（%, 기본 10） */\n  minSize?: number;\n  /** 최대 크기（%, 기본 90） */\n  maxSize?: number;\n  /** 그룹이 주입하는 순번（내부 전용） */\n  index?: number;\n};\n\nconst ResizablePanel = forwardRef<BridgeViewRef, ResizablePanelProps>(\n  (\n    {\n      // 제약 prop은 그룹이 children 순회 시 읽는다 — Panel 본체는 소비하지\n      // 않으므로 구조분해로 버려 View(DOM)에 전개되지 않게 한다.\n      defaultSize: _defaultSize,\n      minSize: _minSize,\n      maxSize: _maxSize,\n      index = -1,\n      className,\n      style,\n      ...props\n    },\n    ref,\n  ) => {\n    const ctx = useResizableContext(\"ResizablePanel\");\n    const size = index >= 0 ? (ctx.sizes[index] ?? null) : null;\n    // 마지막 패널（null）은 잔여 폭 차지 — 나머지는 동적 basis %로 고정.\n    const isLast = size === null;\n    return (\n      <View\n        ref={ref}\n        className={cn(isLast ? \"flex-1\" : \"grow-0 shrink-0\", className)}\n        style={\n          isLast ? style : { ...style, flexBasis: `${size}%` }\n        }\n        {...props}\n      />\n    );\n  },\n);\nResizablePanel.displayName = \"ResizablePanel\";\n\nexport type ResizableHandleProps = ViewProps & {\n  /** 중앙 그립 박스 표시 */\n  withHandle?: boolean;\n  /** 그룹이 주입하는 조절 대상（왼쪽 패널） 순번（내부 전용） */\n  index?: number;\n};\n\nconst ResizableHandle = forwardRef<BridgeViewRef, ResizableHandleProps>(\n  ({ withHandle, index = -1, className, ...props }, ref) => {\n    const ctx = useResizableContext(\"ResizableHandle\");\n    const { dragHandlers } = useDrag({\n      onBegin: () => ctx.beginResize(index),\n      onMove: (event) => ctx.moveResize(index, event),\n    });\n    const horizontal = ctx.direction === \"horizontal\";\n    return (\n      <View\n        ref={ref}\n        accessibilityRole=\"separator\"\n        {...dragHandlers}\n        className={cn(\n          \"relative flex touch-none items-center justify-center select-none\",\n          horizontal\n            ? \"w-1 web:cursor-col-resize\"\n            : \"h-1 web:cursor-row-resize\",\n          className,\n        )}\n        {...props}\n      >\n        {/* 시각 바 — 핸들（히트 영역 w-1/h-1） 안의 w-px/h-px 선. 중앙 배치는\n            절반 오프셋 토큰(-ml-px/-mt-px)으로 결정적으로 표현한다. */}\n        <View\n          aria-hidden\n          className={cn(\n            \"absolute bg-border\",\n            horizontal\n              ? \"inset-y-0 left-1/2 -ml-px w-px\"\n              : \"inset-x-0 top-1/2 -mt-px h-px\",\n          )}\n        />\n        {withHandle && (\n          <View\n            aria-hidden\n            className=\"z-10 flex h-4 w-3 items-center justify-center rounded-xs border border-border bg-border\"\n          >\n            <GripVerticalIcon\n              className={cn(\n                \"size-2.5 text-muted-foreground\",\n                !horizontal && \"rotate-90\",\n              )}\n            />\n          </View>\n        )}\n      </View>\n    );\n  },\n);\nResizableHandle.displayName = \"ResizableHandle\";\n\nexport { ResizablePanelGroup, ResizablePanel, ResizableHandle };\n",
      "type": "registry:ui",
      "target": "components/ui/resizable.tsx"
    }
  ],
  "type": "registry:ui"
}
