{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "splitter",
  "type": "registry:ui",
  "title": "Splitter",
  "description": "한 축을 나눠 쓰는 패널들과 그 사이를 끌어 크기를 바꾸는 구분자입니다. shadcn/ui Resizable 을 기준으로 삼았습니다.",
  "registryDependencies": [
    "@atom/tokens",
    "@atom/lib-bem",
    "@atom/lib-cn",
    "@atom/lib-context"
  ],
  "files": [
    {
      "path": "poc/design-system/implement/src/components/splitter.tsx",
      "content": "\"use client\";\n\nimport {\n  type HTMLAttributes,\n  type KeyboardEvent,\n  type PointerEvent as ReactPointerEvent,\n  type Ref,\n  useCallback,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { mod } from \"../lib/bem\";\nimport { cn } from \"../lib/cn\";\nimport { createStrictContext } from \"../lib/context\";\n\n/* -------------------------------------------------------------------------- */\n/*  Splitter (components/splitter.md, styles/atom/elements/splitter.css)      */\n/*    <Splitter orientation=\"horizontal\" onLayoutChanged={save}>              */\n/*      <Splitter.Panel defaultSize=\"30\" minSize=\"15\" maxSize=\"60\">           */\n/*        <Splitter.Content>목록</Splitter.Content>                            */\n/*      </Splitter.Panel>                                                     */\n/*      <Splitter.Separator withHandle aria-label=\"탐색 영역 크기 조절\" />      */\n/*      <Splitter.Panel>                                                      */\n/*        <Splitter.Content>상세</Splitter.Content>                            */\n/*      </Splitter.Panel>                                                     */\n/*    </Splitter>                                                             */\n/*                                                                            */\n/*  크기 단위는 react-resizable-panels 규칙: 숫자는 px, 단위 없는 문자열은 %,   */\n/*  px · % · em · rem · vh · vw 명시 가능. 내부 배치(layout)는 % 배열입니다.     */\n/* -------------------------------------------------------------------------- */\n\nexport type SplitterOrientation = \"horizontal\" | \"vertical\";\n/** 숫자는 px, 단위 없는 문자열은 %. `px` · `%` · `em` · `rem` · `vh` · `vw` 를 명시할 수도 있습니다. */\nexport type SplitterSize = number | string;\n/** 패널 순서대로의 몫(%) 배열 */\nexport type SplitterLayout = number[];\nexport type SplitterPanelSize = { asPercentage: number; inPixels: number };\n\nconst B = \"atom-splitter\";\nconst PANEL = `${B}__panel`;\nconst EPS = 1e-6;\n/** 화살표 키 한 번의 이동량(%) */\nconst KEY_STEP = 5;\n\ntype PanelConfig = {\n  defaultSize?: SplitterSize;\n  minSize: SplitterSize;\n  maxSize: SplitterSize;\n  collapsible: boolean;\n  collapsedSize: SplitterSize;\n};\ntype Constraints = { min: number; max: number; collapsed: number; collapsible: boolean };\ntype LayoutMap = Record<string, number>;\n\ntype SplitterContextValue = {\n  orientation: SplitterOrientation;\n  disabled: boolean;\n  dragging: boolean;\n  layout: LayoutMap;\n  lastId: string | undefined;\n  registerPanel: (id: string, config: { current: PanelConfig }) => () => void;\n  constraints: (id: string) => Constraints;\n  isCollapsed: (id: string) => boolean;\n  groupPx: () => number;\n  /** prev 패널의 몫을 sizeA 로 바꾸고 next 가 나머지를 가져갑니다. */\n  resizePair: (prev: string, next: string, sizeA: number, final: boolean, base?: LayoutMap) => void;\n  toggleCollapse: (prev: string, next: string) => void;\n  reset: () => void;\n  layoutSnapshot: () => LayoutMap;\n  commitFinal: () => void;\n  setDragging: (on: boolean) => void;\n};\n\nconst [SplitterContext, useSplitterContext] = createStrictContext<SplitterContextValue>(\"Splitter\");\n\n/* --------------------------------- 단위 ----------------------------------- */\n\nconst SIZE_RE = /^\\s*(-?\\d*\\.?\\d+)\\s*(px|%|em|rem|vh|vw)?\\s*$/;\n\n/** 크기 값을 그룹 축 대비 % 로. px 계열인데 그룹 크기를 모르면 undefined */\nfunction toPercent(value: SplitterSize | undefined, groupPx: number, el: Element | null): number | undefined {\n  if (value === undefined) return undefined;\n  if (typeof value === \"number\") return groupPx > 0 ? (value / groupPx) * 100 : undefined;\n  const m = SIZE_RE.exec(value);\n  if (!m) return undefined;\n  const n = Number.parseFloat(m[1]!);\n  const unit = m[2];\n  if (!unit || unit === \"%\") return n;\n  let px = n;\n  if (unit === \"em\") px = n * (el ? Number.parseFloat(getComputedStyle(el).fontSize) || 16 : 16);\n  else if (unit === \"rem\") px = n * (Number.parseFloat(getComputedStyle(document.documentElement).fontSize) || 16);\n  else if (unit === \"vh\") px = (n * window.innerHeight) / 100;\n  else if (unit === \"vw\") px = (n * window.innerWidth) / 100;\n  return groupPx > 0 ? (px / groupPx) * 100 : undefined;\n}\n\nconst approx = (a: number, b: number) => Math.abs(a - b) < 0.01;\nconst round = (n: number) => Math.round(n * 1000) / 1000;\n\nfunction clampPanel(c: Constraints, size: number): number {\n  // collapsible 패널은 minSize 아래로 끌리면 collapsedSize 까지 접힙니다.\n  if (c.collapsible && size < c.min - EPS) return c.collapsed;\n  return Math.min(c.max, Math.max(c.min, size));\n}\n\nfunction isValid(c: Constraints, size: number): boolean {\n  return (c.collapsible && approx(size, c.collapsed)) || (size >= c.min - EPS && size <= c.max + EPS);\n}\n\nfunction panelIds(group: HTMLElement | null): string[] {\n  if (!group) return [];\n  return Array.from(group.children)\n    .filter((el) => el.classList.contains(PANEL))\n    .map((el) => el.id);\n}\n\nfunction adjacentPanel(el: Element | null, dir: \"previousElementSibling\" | \"nextElementSibling\"): string | undefined {\n  let cur = el?.[dir] ?? null;\n  while (cur && !cur.classList.contains(PANEL)) cur = cur[dir];\n  return cur?.id || undefined;\n}\n\n/* --------------------------------- Group ---------------------------------- */\n\nexport type SplitterProps = HTMLAttributes<HTMLDivElement> & {\n  ref?: Ref<HTMLDivElement>;\n  /** 패널이 나뉘는 축입니다. Group 에 붙습니다. */\n  orientation?: SplitterOrientation;\n  /** Group 에 주면 전체를, Separator 에 주면 그 하나를 잠급니다. */\n  disabled?: boolean;\n  /** 처음 배치입니다. 저장해 둔 값을 넣으면 새로고침 뒤에도 유지됩니다. (숫자 배열은 onLayoutChanged 가 주는 % 배치, 문자열은 크기 단위 규칙) */\n  defaultLayout?: number[] | string[];\n  /** 드래그하는 동안 포인터가 움직일 때마다 호출됩니다. */\n  onLayoutChange?: (layout: SplitterLayout) => void;\n  /** 드래그가 끝난 뒤 한 번 호출됩니다. 저장은 이쪽에서 합니다. */\n  onLayoutChanged?: (layout: SplitterLayout) => void;\n};\n\nfunction SplitterRoot({\n  ref,\n  orientation = \"horizontal\",\n  disabled = false,\n  defaultLayout,\n  onLayoutChange,\n  onLayoutChanged,\n  className,\n  children,\n  ...rest\n}: SplitterProps) {\n  const groupRef = useRef<HTMLDivElement | null>(null);\n  const panels = useRef(new Map<string, { current: PanelConfig }>());\n  const [version, setVersion] = useState(0);\n  const [state, setState] = useState<{ layout: LayoutMap; order: string[] }>({ layout: {}, order: [] });\n  const layoutRef = useRef<LayoutMap>({});\n  const initialRef = useRef<LayoutMap>({});\n  const restoreRef = useRef<LayoutMap>({});\n  const [dragging, setDragging] = useState(false);\n\n  const callbacks = useRef({ onLayoutChange, onLayoutChanged, defaultLayout });\n  useLayoutEffect(() => {\n    callbacks.current = { onLayoutChange, onLayoutChanged, defaultLayout };\n  });\n\n  const groupPx = useCallback(() => {\n    const el = groupRef.current;\n    if (!el) return 0;\n    const rect = el.getBoundingClientRect();\n    return orientation === \"horizontal\" ? rect.width : rect.height;\n  }, [orientation]);\n\n  const constraints = useCallback(\n    (id: string): Constraints => {\n      const cfg = panels.current.get(id)?.current;\n      const px = groupPx();\n      const el = groupRef.current;\n      return {\n        min: toPercent(cfg?.minSize, px, el) ?? 0,\n        max: toPercent(cfg?.maxSize, px, el) ?? 100,\n        collapsed: toPercent(cfg?.collapsedSize, px, el) ?? 0,\n        collapsible: cfg?.collapsible ?? false,\n      };\n    },\n    [groupPx],\n  );\n\n  const commit = useCallback((next: LayoutMap, final: boolean) => {\n    layoutRef.current = next;\n    const order = panelIds(groupRef.current);\n    setState({ layout: next, order });\n    const arr = order.map((id) => round(next[id] ?? 0));\n    callbacks.current.onLayoutChange?.(arr);\n    if (final) callbacks.current.onLayoutChanged?.(arr);\n  }, []);\n\n  // 패널이 등록·해제되면 처음 배치를 계산합니다 (defaultLayout → defaultSize → 나머지 균등).\n  useLayoutEffect(() => {\n    const ids = panelIds(groupRef.current);\n    const cur = layoutRef.current;\n    if (ids.length === Object.keys(cur).length && ids.every((id) => id in cur)) {\n      setState((s) => (s.order.join() === ids.join() ? s : { ...s, order: ids }));\n      return;\n    }\n    const px = groupPx();\n    const el = groupRef.current;\n    const dl = callbacks.current.defaultLayout;\n    const sizes = ids.map((id, i) => {\n      if (id in cur) return cur[id];\n      const v = dl?.[i];\n      if (v !== undefined) return typeof v === \"number\" ? v : toPercent(v, px, el);\n      return toPercent(panels.current.get(id)?.current.defaultSize, px, el);\n    });\n    const fixed = sizes.reduce<number>((n, s) => n + (s ?? 0), 0);\n    const auto = sizes.filter((s) => s === undefined).length;\n    const share = auto ? Math.max(0, 100 - fixed) / auto : 0;\n    const next: LayoutMap = {};\n    ids.forEach((id, i) => {\n      const c = constraints(id);\n      const s = sizes[i] ?? share;\n      next[id] = c.collapsible && approx(s, c.collapsed) ? c.collapsed : clampPanel(c, s);\n    });\n    // 마지막 패널이 남는 몫을 가져갑니다 (인라인 flex-basis 없이 늘어나는 패널).\n    const last = ids[ids.length - 1];\n    if (last !== undefined) {\n      const others = ids.slice(0, -1).reduce((n, id) => n + (next[id] ?? 0), 0);\n      next[last] = Math.max(0, 100 - others);\n    }\n    initialRef.current = { ...initialRef.current, ...next };\n    layoutRef.current = next;\n    setState({ layout: next, order: ids });\n  }, [version, groupPx, constraints]);\n\n  const registerPanel = useCallback((id: string, config: { current: PanelConfig }) => {\n    panels.current.set(id, config);\n    setVersion((v) => v + 1);\n    return () => {\n      panels.current.delete(id);\n      const { [id]: _removed, ...restLayout } = layoutRef.current;\n      layoutRef.current = restLayout;\n      setVersion((v) => v + 1);\n    };\n  }, []);\n\n  const pairLayout = useCallback(\n    (base: LayoutMap, prev: string, next: string, sizeA: number): LayoutMap | null => {\n      const a = base[prev];\n      const b = base[next];\n      if (a === undefined || b === undefined) return null;\n      const total = a + b;\n      const ca = constraints(prev);\n      const cb = constraints(next);\n      let A = clampPanel(ca, Math.min(total, Math.max(0, sizeA)));\n      let Bsize = total - A;\n      const clampedB = clampPanel(cb, Bsize);\n      if (!approx(clampedB, Bsize)) {\n        A = total - clampedB;\n        if (!isValid(ca, A)) return null;\n        Bsize = clampedB;\n      }\n      return { ...base, [prev]: A, [next]: Bsize };\n    },\n    [constraints],\n  );\n\n  const ctx: SplitterContextValue = {\n    orientation,\n    disabled,\n    dragging,\n    layout: state.layout,\n    lastId: state.order[state.order.length - 1],\n    registerPanel,\n    constraints,\n    groupPx,\n    isCollapsed: (id) => {\n      const c = constraints(id);\n      const size = state.layout[id];\n      return c.collapsible && size !== undefined && approx(size, c.collapsed);\n    },\n    resizePair: (prev, next, sizeA, final, base = layoutRef.current) => {\n      const out = pairLayout(base, prev, next, sizeA);\n      if (out && (!approx(out[prev]!, layoutRef.current[prev] ?? -1) || final)) commit(out, final);\n    },\n    toggleCollapse: (prev, next) => {\n      const base = layoutRef.current;\n      const a = base[prev];\n      const b = base[next];\n      if (a === undefined || b === undefined) return;\n      const ca = constraints(prev);\n      const cb = constraints(next);\n      const target = ca.collapsible ? prev : cb.collapsible ? next : undefined;\n      if (!target) return;\n      const c = target === prev ? ca : cb;\n      const size = target === prev ? a : b;\n      let nextSize: number;\n      if (approx(size, c.collapsed)) {\n        nextSize = restoreRef.current[target] ?? initialRef.current[target] ?? c.min;\n        if (approx(nextSize, c.collapsed)) nextSize = Math.max(c.min, c.collapsed);\n      } else {\n        restoreRef.current[target] = size;\n        nextSize = c.collapsed;\n      }\n      const sizeA = target === prev ? nextSize : a + b - nextSize;\n      const out = pairLayout(base, prev, next, sizeA);\n      if (out) commit(out, true);\n    },\n    reset: () => {\n      const ids = panelIds(groupRef.current);\n      const next: LayoutMap = {};\n      for (const id of ids) next[id] = initialRef.current[id] ?? layoutRef.current[id] ?? 0;\n      commit(next, true);\n    },\n    layoutSnapshot: () => layoutRef.current,\n    commitFinal: () => {\n      const arr = panelIds(groupRef.current).map((id) => round(layoutRef.current[id] ?? 0));\n      callbacks.current.onLayoutChanged?.(arr);\n    },\n    setDragging,\n  };\n\n  return (\n    <SplitterContext value={ctx}>\n      <div\n        {...rest}\n        ref={(el) => {\n          groupRef.current = el;\n          if (typeof ref === \"function\") ref(el);\n          else if (ref) ref.current = el;\n        }}\n        data-disabled={disabled ? \"\" : undefined}\n        data-dragging={dragging ? \"\" : undefined}\n        className={cn(B, mod(B, \"orientation\", orientation, \"horizontal\"), className)}\n      >\n        {children}\n      </div>\n    </SplitterContext>\n  );\n}\n\n/* --------------------------------- Panel ---------------------------------- */\n\nexport type SplitterPanelProps = Omit<HTMLAttributes<HTMLDivElement>, \"onResize\"> & {\n  ref?: Ref<HTMLDivElement>;\n  /** Panel 의 처음 몫입니다. 숫자는 px, 단위 없는 문자열은 %. */\n  defaultSize?: SplitterSize;\n  /** Panel 의 최소 몫입니다. */\n  minSize?: SplitterSize;\n  /** Panel 의 최대 몫입니다. */\n  maxSize?: SplitterSize;\n  /** `minSize` 아래로 끌리면 접히게 합니다. */\n  collapsible?: boolean;\n  /** 접혔을 때의 몫입니다. */\n  collapsedSize?: SplitterSize;\n  /** Panel 의 몫이 바뀔 때 호출됩니다. */\n  onResize?: (size: SplitterPanelSize) => void;\n};\n\n/** Panel — 자기 몫을 차지합니다. 크기는 인라인 flex-basis 로 들어옵니다. */\nfunction SplitterPanel({\n  ref,\n  id,\n  defaultSize,\n  minSize = \"0%\",\n  maxSize = \"100%\",\n  collapsible = false,\n  collapsedSize = \"0%\",\n  onResize,\n  className,\n  style,\n  children,\n  ...rest\n}: SplitterPanelProps) {\n  const ctx = useSplitterContext(\"Splitter.Panel\");\n  const autoId = useId();\n  const panelId = id ?? `atom-splitter-panel-${autoId.replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  const config = useRef<PanelConfig>({ defaultSize, minSize, maxSize, collapsible, collapsedSize });\n  config.current = { defaultSize, minSize, maxSize, collapsible, collapsedSize };\n\n  const { registerPanel } = ctx;\n  useLayoutEffect(() => registerPanel(panelId, config), [registerPanel, panelId]);\n\n  const size = ctx.layout[panelId];\n  const collapsed = ctx.isCollapsed(panelId);\n  const onResizeRef = useRef(onResize);\n  useLayoutEffect(() => {\n    onResizeRef.current = onResize;\n  });\n  const { groupPx } = ctx;\n  useEffect(() => {\n    if (size === undefined) return;\n    onResizeRef.current?.({ asPercentage: round(size), inPixels: (size / 100) * groupPx() });\n  }, [size, groupPx]);\n\n  const sized = size !== undefined && panelId !== ctx.lastId;\n  return (\n    <div\n      {...rest}\n      ref={ref}\n      id={panelId}\n      data-collapsed={collapsed ? \"\" : undefined}\n      className={cn(PANEL, className)}\n      style={sized ? { flexBasis: `${round(size)}%`, flexGrow: 0, ...style } : style}\n    >\n      {children}\n    </div>\n  );\n}\n\n/* ------------------------------- Separator -------------------------------- */\n\nexport type SplitterSeparatorProps = HTMLAttributes<HTMLDivElement> & {\n  ref?: Ref<HTMLDivElement>;\n  /** Separator 가운데에 Grip 을 그립니다. */\n  withHandle?: boolean;\n  /** 이 Separator 하나를 잠급니다. */\n  disabled?: boolean;\n  /** 더블클릭으로 기본 비율에 돌아가는 동작을 끕니다. */\n  disableDoubleClick?: boolean;\n};\n\n/** 원본 데모의 Grip 점 여섯 개 (스프라이트에 없는 인라인 SVG) */\nfunction GripDots({ vertical }: { vertical: boolean }) {\n  return vertical ? (\n    <svg viewBox=\"0 0 14 7\" aria-hidden=\"true\">\n      {[3, 7, 11].flatMap((cx) =>\n        [2, 5].map((cy) => <circle key={`${cx}-${cy}`} cx={cx} cy={cy} r=\"0.9\" fill=\"currentColor\" />),\n      )}\n    </svg>\n  ) : (\n    <svg viewBox=\"0 0 7 14\" aria-hidden=\"true\">\n      {[3, 7, 11].flatMap((cy) =>\n        [2, 5].map((cx) => <circle key={`${cx}-${cy}`} cx={cx} cy={cy} r=\"0.9\" fill=\"currentColor\" />),\n      )}\n    </svg>\n  );\n}\n\n/** Separator — 9px 히트 영역 안에 1px 선. role=\"separator\" (WAI-ARIA window splitter) */\nfunction SplitterSeparator({\n  ref,\n  withHandle = false,\n  disabled = false,\n  disableDoubleClick = false,\n  className,\n  children,\n  onKeyDown,\n  onPointerDown,\n  onDoubleClick,\n  ...rest\n}: SplitterSeparatorProps) {\n  const ctx = useSplitterContext(\"Splitter.Separator\");\n  const elRef = useRef<HTMLDivElement | null>(null);\n  const [pair, setPair] = useState<{ prev?: string; next?: string }>({});\n  const [dragging, setDragging] = useState(false);\n  const cleanup = useRef<(() => void) | null>(null);\n\n  // 앞뒤 패널은 DOM 순서로 찾습니다 (Separator 는 패널 사이마다 하나).\n  useLayoutEffect(() => {\n    const prev = adjacentPanel(elRef.current, \"previousElementSibling\");\n    const next = adjacentPanel(elRef.current, \"nextElementSibling\");\n    setPair((p) => (p.prev === prev && p.next === next ? p : { prev, next }));\n  });\n  useEffect(() => () => cleanup.current?.(), []);\n\n  const locked = disabled || ctx.disabled;\n  const horizontal = ctx.orientation === \"horizontal\";\n  const { prev, next } = pair;\n  const size = prev ? ctx.layout[prev] : undefined;\n  const c = prev ? ctx.constraints(prev) : undefined;\n\n  const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n    onKeyDown?.(e);\n    if (e.defaultPrevented || locked || !prev || !next || size === undefined || !c) return;\n    const dec = horizontal ? \"ArrowLeft\" : \"ArrowUp\";\n    const inc = horizontal ? \"ArrowRight\" : \"ArrowDown\";\n    let target: number | undefined;\n    if (e.key === dec) target = size - KEY_STEP;\n    else if (e.key === inc) target = size + KEY_STEP;\n    else if (e.key === \"Home\") target = c.min;\n    else if (e.key === \"End\") target = c.max;\n    else if (e.key === \"Enter\") {\n      e.preventDefault();\n      ctx.toggleCollapse(prev, next);\n      return;\n    }\n    if (target === undefined) return;\n    e.preventDefault();\n    // 접힌 패널을 화살표로 늘리면 minSize 부터 다시 시작합니다.\n    if (c.collapsible && approx(size, c.collapsed) && e.key === inc) target = Math.max(target, c.min);\n    ctx.resizePair(prev, next, target, true);\n  };\n\n  const handlePointerDown = (e: ReactPointerEvent<HTMLDivElement>) => {\n    onPointerDown?.(e);\n    if (e.defaultPrevented || locked || !prev || !next || e.button !== 0) return;\n    e.preventDefault();\n    const start = horizontal ? e.clientX : e.clientY;\n    const base = ctx.layoutSnapshot();\n    const startSize = base[prev] ?? 0;\n    const px = ctx.groupPx();\n    setDragging(true);\n    ctx.setDragging(true);\n    const move = (ev: PointerEvent) => {\n      if (px <= 0) return;\n      const delta = (((horizontal ? ev.clientX : ev.clientY) - start) / px) * 100;\n      ctx.resizePair(prev, next, startSize + delta, false, base);\n    };\n    const up = () => {\n      cleanup.current?.();\n      ctx.commitFinal();\n    };\n    cleanup.current = () => {\n      window.removeEventListener(\"pointermove\", move);\n      window.removeEventListener(\"pointerup\", up);\n      window.removeEventListener(\"pointercancel\", up);\n      cleanup.current = null;\n      setDragging(false);\n      ctx.setDragging(false);\n    };\n    window.addEventListener(\"pointermove\", move);\n    window.addEventListener(\"pointerup\", up);\n    window.addEventListener(\"pointercancel\", up);\n    elRef.current?.focus();\n  };\n\n  return (\n    <div\n      {...rest}\n      ref={(el) => {\n        elRef.current = el;\n        if (typeof ref === \"function\") ref(el);\n        else if (ref) ref.current = el;\n      }}\n      role=\"separator\"\n      tabIndex={0}\n      aria-orientation={horizontal ? \"vertical\" : \"horizontal\"}\n      aria-controls={prev}\n      aria-valuenow={size === undefined ? undefined : Math.round(size)}\n      aria-valuemin={c ? Math.round(c.collapsible ? Math.min(c.collapsed, c.min) : c.min) : undefined}\n      aria-valuemax={c ? Math.round(c.max) : undefined}\n      aria-disabled={locked || undefined}\n      data-disabled={disabled ? \"\" : undefined}\n      data-dragging={dragging ? \"\" : undefined}\n      className={cn(`${B}__separator`, className)}\n      onKeyDown={handleKeyDown}\n      onPointerDown={handlePointerDown}\n      onDoubleClick={(e) => {\n        onDoubleClick?.(e);\n        if (e.defaultPrevented || locked || disableDoubleClick) return;\n        ctx.reset();\n      }}\n    >\n      {withHandle && (\n        <span className={`${B}__grip`}>\n          <GripDots vertical={!horizontal} />\n        </span>\n      )}\n      {children}\n    </div>\n  );\n}\n\n/* -------------------------------- Content --------------------------------- */\n\nexport type SplitterContentProps = HTMLAttributes<HTMLDivElement> & { ref?: Ref<HTMLDivElement> };\n\n/** Content — 패널 안에서 따로 스크롤되는 상자입니다. 선택 사항입니다. */\nfunction SplitterContent({ ref, className, ...rest }: SplitterContentProps) {\n  useSplitterContext(\"Splitter.Content\");\n  return <div {...rest} ref={ref} className={cn(`${B}__content`, className)} />;\n}\n\nexport const Splitter = Object.assign(SplitterRoot, {\n  Panel: SplitterPanel,\n  Separator: SplitterSeparator,\n  Content: SplitterContent,\n});\n",
      "type": "registry:ui",
      "target": "components/ui/splitter.tsx"
    },
    {
      "path": "poc/design-system/implement/src/styles/atom/elements/splitter.css",
      "content": "/* ============================================================================\n   Element — Splitter\n   ----------------------------------------------------------------------------\n   Two or more panels sharing one axis, with a draggable separator between each\n   pair. Modelled on shadcn/ui Resizable (ResizablePanelGroup / ResizablePanel /\n   ResizableHandle), which wraps react-resizable-panels.\n\n   This file owns the shape and the states. It does not own the sizing: the\n   panel's share of the axis arrives as an inline `flex-basis` that the resize\n   logic rewrites on every pointer move. A stylesheet that also declared widths\n   would fight that script on the first drag.\n\n   atom-splitter\n     --orientation_{horizontal|vertical}     horizontal is the default\n     [data-disabled]                         the whole group stops resizing\n     __panel     [data-collapsed]\n     __separator role=\"separator\"  [data-dragging] [data-disabled]\n     __grip      the visible handle — shadcn's `withHandle`\n     __content   optional inner box that scrolls independently\n\n   Horizontal means the panels sit side by side and the separator moves left and\n   right — the same reading as shadcn's `orientation`, not the direction the\n   separator line is drawn.\n   ========================================================================== */\n\n.atom-splitter {\n  display: flex;\n  width: 100%;\n  height: 100%;\n  min-width: 0;\n  min-height: 0;\n  border-radius: var(--atom-radius-r2);\n  background: var(--atom-bg-layer);\n  box-shadow: inset 0 0 0 1px var(--atom-border-subtle);\n  overflow: hidden;\n}\n\n.atom-splitter--orientation_vertical {\n  flex-direction: column;\n}\n\n/* --- panel --------------------------------------------------------------- */\n/* `min-*: 0` is what lets a panel shrink under its content instead of pinning\n   the group at the widest child — the constraint that should hold is minSize,\n   and that lives in the resize logic, not here. */\n.atom-splitter__panel {\n  flex: 1 1 0;\n  min-width: 0;\n  min-height: 0;\n  overflow: hidden;\n}\n\n.atom-splitter__content {\n  width: 100%;\n  height: 100%;\n  padding: var(--atom-x4);\n  overflow: auto;\n  font-size: 14px;\n  color: var(--atom-fg-neutral);\n}\n\n/* A collapsed panel keeps its box so the separator still has something to sit\n   against; only its content goes. Removing the panel outright would renumber\n   the separators and lose the expand target. */\n.atom-splitter__panel[data-collapsed] {\n  flex-basis: 0 !important;\n  flex-grow: 0;\n}\n\n.atom-splitter__panel[data-collapsed] > .atom-splitter__content {\n  display: none;\n}\n\n/* --- separator ----------------------------------------------------------- */\n/* The hit area is deliberately wider than the 1px line it draws. A separator\n   only as wide as its own hairline is unusable with a mouse and impossible with\n   a trackpad — the same reason Grid's column resize handle is 9px. */\n.atom-splitter__separator {\n  position: relative;\n  flex: 0 0 auto;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 9px;\n  margin-inline: -4px;\n  cursor: col-resize;\n  touch-action: none;\n  z-index: 1;\n}\n\n.atom-splitter--orientation_vertical .atom-splitter__separator {\n  width: auto;\n  height: 9px;\n  margin-inline: 0;\n  margin-block: -4px;\n  cursor: row-resize;\n}\n\n.atom-splitter__separator::before {\n  content: \"\";\n  position: absolute;\n  inset-block: 0;\n  inset-inline-start: 4px;\n  width: 1px;\n  background: var(--atom-border-subtle);\n  transition: background-color var(--atom-duration-fast) ease;\n}\n\n.atom-splitter--orientation_vertical .atom-splitter__separator::before {\n  inset: 4px 0 auto 0;\n  width: auto;\n  height: 1px;\n}\n\n/* --- grip ---------------------------------------------------------------- */\n/* shadcn's `withHandle`. Without it the separator is discoverable only by\n   hovering the right pixel, which is fine between two obviously-draggable\n   panes and not fine anywhere else. */\n.atom-splitter__grip {\n  position: relative;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 11px;\n  height: 22px;\n  border-radius: var(--atom-radius-r1);\n  background: var(--atom-bg-neutral-weak);\n  box-shadow: inset 0 0 0 1px var(--atom-border-subtle);\n  color: var(--atom-fg-neutral-muted);\n  transition: background-color var(--atom-duration-fast) ease,\n    color var(--atom-duration-fast) ease;\n}\n\n.atom-splitter--orientation_vertical .atom-splitter__grip {\n  width: 22px;\n  height: 11px;\n}\n\n.atom-splitter__grip svg {\n  width: 7px;\n  height: 14px;\n  display: block;\n}\n\n.atom-splitter--orientation_vertical .atom-splitter__grip svg {\n  width: 14px;\n  height: 7px;\n}\n\n/* --- states -------------------------------------------------------------- */\n.atom-splitter__separator:hover::before,\n.atom-splitter__separator[data-dragging]::before {\n  background: var(--atom-fg-brand);\n}\n\n.atom-splitter__separator[data-dragging]::before {\n  width: 2px;\n}\n\n.atom-splitter--orientation_vertical\n  .atom-splitter__separator[data-dragging]::before {\n  width: auto;\n  height: 2px;\n}\n\n.atom-splitter__separator:hover .atom-splitter__grip,\n.atom-splitter__separator[data-dragging] .atom-splitter__grip {\n  background: var(--atom-bg-layer-raised);\n  color: var(--atom-fg-neutral);\n}\n\n/* The separator takes focus, so it needs a ring of its own — it has no label or\n   text for the browser default to wrap. */\n.atom-splitter__separator:focus-visible {\n  outline: 2px solid var(--atom-fg-brand);\n  outline-offset: -1px;\n  border-radius: var(--atom-radius-r1);\n}\n\n.atom-splitter__separator[data-disabled],\n.atom-splitter[data-disabled] .atom-splitter__separator {\n  cursor: default;\n  pointer-events: none;\n}\n\n.atom-splitter__separator[data-disabled]::before,\n.atom-splitter[data-disabled] .atom-splitter__separator::before {\n  background: var(--atom-border-subtle);\n}\n\n.atom-splitter__separator[data-disabled] .atom-splitter__grip,\n.atom-splitter[data-disabled] .atom-splitter__grip {\n  opacity: 0.4;\n}\n\n/* A drag that starts on the separator keeps going over the panels, so the\n   cursor has to survive leaving the 9px strip — and text must not select while\n   it does. shadcn's `disableCursor` turns the first half of this off. */\n.atom-splitter[data-dragging] {\n  user-select: none;\n}\n\n.atom-splitter[data-dragging]:not(.atom-splitter--orientation_vertical) {\n  cursor: col-resize;\n}\n\n.atom-splitter--orientation_vertical[data-dragging] {\n  cursor: row-resize;\n}\n\n/* --- nesting ------------------------------------------------------------- */\n/* A splitter inside a panel is the normal way to build a two-axis layout. The\n   inner one drops the outer's surface so the two do not stack borders. */\n.atom-splitter__panel > .atom-splitter {\n  border-radius: 0;\n  background: none;\n  box-shadow: none;\n}\n",
      "type": "registry:file",
      "target": "styles/atom/elements/splitter.css"
    }
  ],
  "categories": [
    "display"
  ],
  "meta": {
    "version": "0.1.0",
    "kind": "component",
    "status": "stable",
    "specSource": "measured",
    "parts": [
      "Splitter",
      "Splitter.Panel",
      "Splitter.Separator",
      "Splitter.Content"
    ],
    "docs": "poc/design-system/components/splitter.md"
  }
}
