{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gummy-resizable",
  "type": "registry:ui",
  "title": "Gummy Resizable",
  "description": "Controlled or uncontrolled two-panel layout with pointer resizing, keyboard separator semantics, bounds, horizontal and vertical modes, and RTL.",
  "registryDependencies": [
    "https://gummyui.dev/r/gummy-primitives-styles.json"
  ],
  "files": [
    {
      "path": "components/ui/GummyResizable.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nfunction joinClassNames(...values: Array<string | false | null | undefined>) {\n  return values.filter(Boolean).join(\" \");\n}\n\ntype ResizableContextValue = {\n  orientation: \"horizontal\" | \"vertical\";\n  direction: \"ltr\" | \"rtl\";\n  size: number;\n  minSize: number;\n  maxSize: number;\n  rootRef: React.RefObject<HTMLDivElement | null>;\n  setSize: (value: number) => void;\n};\n\nconst ResizableContext = React.createContext<ResizableContextValue | null>(null);\n\nfunction useResizable() {\n  const context = React.useContext(ResizableContext);\n  if (!context) throw new Error(\"Gummy Resizable parts must be used inside GummyResizablePanelGroup.\");\n  return context;\n}\n\nexport type GummyResizablePanelGroupProps = Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"onChange\" | \"dir\"\n> & {\n  orientation?: \"horizontal\" | \"vertical\";\n  direction?: \"ltr\" | \"rtl\";\n  size?: number;\n  defaultSize?: number;\n  minSize?: number;\n  maxSize?: number;\n  onSizeChange?: (size: number) => void;\n};\n\nexport const GummyResizablePanelGroup = React.forwardRef<\n  HTMLDivElement,\n  GummyResizablePanelGroupProps\n>(function GummyResizablePanelGroup(\n  {\n    orientation = \"horizontal\",\n    direction = \"ltr\",\n    size: controlledSize,\n    defaultSize = 50,\n    minSize = 20,\n    maxSize = 80,\n    onSizeChange,\n    className,\n    style,\n    children,\n    ...props\n  },\n  ref,\n) {\n  const safeMin = Math.max(0, Math.min(minSize, 95));\n  const safeMax = Math.max(safeMin, Math.min(maxSize, 100));\n  const clamp = React.useCallback(\n    (value: number) => Math.min(safeMax, Math.max(safeMin, value)),\n    [safeMax, safeMin],\n  );\n  const [internalSize, setInternalSize] = React.useState(() => clamp(defaultSize));\n  const currentSize = clamp(controlledSize ?? internalSize);\n  const rootRef = React.useRef<HTMLDivElement | null>(null);\n  const setRootRef = React.useCallback(\n    (node: HTMLDivElement | null) => {\n      rootRef.current = node;\n      if (typeof ref === \"function\") ref(node);\n      else if (ref) ref.current = node;\n    },\n    [ref],\n  );\n  const setSize = React.useCallback(\n    (next: number) => {\n      const clamped = clamp(next);\n      if (controlledSize === undefined) setInternalSize(clamped);\n      onSizeChange?.(clamped);\n    },\n    [clamp, controlledSize, onSizeChange],\n  );\n  const context = React.useMemo(\n    () => ({\n      orientation,\n      direction,\n      size: currentSize,\n      minSize: safeMin,\n      maxSize: safeMax,\n      rootRef,\n      setSize,\n    }),\n    [currentSize, direction, orientation, safeMax, safeMin, setSize],\n  );\n  return (\n    <ResizableContext.Provider value={context}>\n      <div\n        {...props}\n        ref={setRootRef}\n        dir={direction}\n        className={joinClassNames(\"gummy-resizable\", className)}\n        data-orientation={orientation}\n        style={{ ...style, \"--gummy-resizable-size\": `${currentSize}%` } as React.CSSProperties}\n      >\n        {children}\n      </div>\n    </ResizableContext.Provider>\n  );\n});\n\nexport type GummyResizablePanelProps = React.HTMLAttributes<HTMLDivElement> & {\n  order: \"first\" | \"second\";\n};\n\nexport const GummyResizablePanel = React.forwardRef<HTMLDivElement, GummyResizablePanelProps>(\n  function GummyResizablePanel({ order, className, ...props }, ref) {\n    useResizable();\n    return (\n      <div\n        {...props}\n        ref={ref}\n        className={joinClassNames(\"gummy-resizable__panel\", className)}\n        data-order={order}\n      />\n    );\n  },\n);\n\nexport type GummyResizableHandleProps = React.HTMLAttributes<HTMLDivElement> & {\n  label?: string;\n  step?: number;\n};\n\nexport const GummyResizableHandle = React.forwardRef<HTMLDivElement, GummyResizableHandleProps>(\n  function GummyResizableHandle(\n    { label = \"Resize panels\", step = 2, className, onKeyDown, onPointerDown, onPointerMove, onPointerUp, ...props },\n    ref,\n  ) {\n    const context = useResizable();\n    const dragging = React.useRef(false);\n\n    function updateFromPointer(event: React.PointerEvent<HTMLDivElement>) {\n      const bounds = context.rootRef.current?.getBoundingClientRect();\n      if (!bounds) return;\n      const raw = context.orientation === \"horizontal\"\n        ? ((event.clientX - bounds.left) / Math.max(bounds.width, 1)) * 100\n        : ((event.clientY - bounds.top) / Math.max(bounds.height, 1)) * 100;\n      context.setSize(\n        context.orientation === \"horizontal\" && context.direction === \"rtl\" ? 100 - raw : raw,\n      );\n    }\n\n    return (\n      <div\n        {...props}\n        ref={ref}\n        role=\"separator\"\n        tabIndex={0}\n        aria-label={label}\n        aria-orientation={context.orientation}\n        aria-valuemin={context.minSize}\n        aria-valuemax={context.maxSize}\n        aria-valuenow={Math.round(context.size)}\n        className={joinClassNames(\"gummy-resizable__handle\", className)}\n        data-orientation={context.orientation}\n        onPointerDown={(event) => {\n          onPointerDown?.(event);\n          if (event.defaultPrevented) return;\n          dragging.current = true;\n          event.currentTarget.setPointerCapture?.(event.pointerId);\n          updateFromPointer(event);\n        }}\n        onPointerMove={(event) => {\n          onPointerMove?.(event);\n          if (dragging.current && !event.defaultPrevented) updateFromPointer(event);\n        }}\n        onPointerUp={(event) => {\n          onPointerUp?.(event);\n          dragging.current = false;\n          event.currentTarget.releasePointerCapture?.(event.pointerId);\n        }}\n        onPointerCancel={() => {\n          dragging.current = false;\n        }}\n        onKeyDown={(event) => {\n          onKeyDown?.(event);\n          if (event.defaultPrevented) return;\n          const amount = event.shiftKey ? step * 5 : step;\n          let delta = 0;\n          if (context.orientation === \"horizontal\") {\n            if (event.key === \"ArrowLeft\") delta = context.direction === \"rtl\" ? amount : -amount;\n            if (event.key === \"ArrowRight\") delta = context.direction === \"rtl\" ? -amount : amount;\n          } else {\n            if (event.key === \"ArrowUp\") delta = -amount;\n            if (event.key === \"ArrowDown\") delta = amount;\n          }\n          if (event.key === \"Home\") {\n            event.preventDefault();\n            context.setSize(context.minSize);\n          } else if (event.key === \"End\") {\n            event.preventDefault();\n            context.setSize(context.maxSize);\n          } else if (delta) {\n            event.preventDefault();\n            context.setSize(context.size + delta);\n          }\n        }}\n      >\n        <span aria-hidden=\"true\">⋮</span>\n      </div>\n    );\n  },\n);\n\nexport const GummyResizable = GummyResizablePanelGroup;\n\nGummyResizablePanelGroup.displayName = \"GummyResizablePanelGroup\";\nGummyResizablePanel.displayName = \"GummyResizablePanel\";\nGummyResizableHandle.displayName = \"GummyResizableHandle\";\n"
    }
  ]
}
