{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "elements-modal-size",
  "type": "registry:component",
  "title": "Elements Modal Size",
  "description": "Shared resizing and stored size for the assistant modal.",
  "files": [
    {
      "type": "registry:component",
      "path": "components/assistant-ui/utils/modal-size.ts",
      "sourcePath": "packages/ui/src/components/react/assistant-ui/utils/modal-size.ts",
      "content": "\"use client\";\n\nimport {\n  type KeyboardEvent,\n  type PointerEvent,\n  type RefObject,\n  useRef,\n  useState,\n} from \"react\";\n\nexport type ModalView = \"thread\" | \"list\";\n\ntype ModalSize = { readonly width: number; readonly height: number };\n\ntype ResizeDrag = {\n  readonly pointerId: number;\n  readonly x: number;\n  readonly y: number;\n  readonly width: number;\n  readonly height: number;\n  readonly direction: 1 | -1;\n  last: ModalSize | null;\n};\n\nconst SIZE_STORAGE_KEY = \"aui-modal-size\";\nconst MIN_SIZE: ModalSize = { width: 320, height: 400 };\n// The trigger and its offsets sit below the modal, so the height leaves room for them.\nconst VIEWPORT_INSET: ModalSize = { width: 32, height: 96 };\nconst RESIZE_STEP = 16;\n\nconst clampSize = ({ width, height }: ModalSize): ModalSize => ({\n  width: Math.round(\n    Math.max(\n      MIN_SIZE.width,\n      Math.min(width, window.innerWidth - VIEWPORT_INSET.width),\n    ),\n  ),\n  height: Math.round(\n    Math.max(\n      MIN_SIZE.height,\n      Math.min(height, window.innerHeight - VIEWPORT_INSET.height),\n    ),\n  ),\n});\n\nconst readStoredSize = (): ModalSize | null => {\n  if (typeof window === \"undefined\") return null;\n  try {\n    const stored: unknown = JSON.parse(\n      window.localStorage.getItem(SIZE_STORAGE_KEY) ?? \"null\",\n    );\n    if (typeof stored !== \"object\" || stored === null) return null;\n    const { width, height } = stored as Record<string, unknown>;\n    if (typeof width !== \"number\" || typeof height !== \"number\") return null;\n    return clampSize({ width, height });\n  } catch {\n    return null;\n  }\n};\n\nconst storeSize = (size: ModalSize | null) => {\n  try {\n    if (size) {\n      window.localStorage.setItem(SIZE_STORAGE_KEY, JSON.stringify(size));\n    } else {\n      window.localStorage.removeItem(SIZE_STORAGE_KEY);\n    }\n  } catch {\n    // Without storage the size lasts until the page reloads.\n  }\n};\n\nconst isRtl = (element: Element) =>\n  getComputedStyle(element).direction === \"rtl\";\n\nexport const useModalSize = (contentRef: RefObject<HTMLDivElement | null>) => {\n  const [size, setSize] = useState(readStoredSize);\n  const dragRef = useRef<ResizeDrag | null>(null);\n\n  const commitSize = (next: ModalSize | null) => {\n    setSize(next);\n    storeSize(next);\n  };\n\n  const sizeFromPointer = (\n    drag: ResizeDrag,\n    event: PointerEvent<HTMLElement>,\n  ) =>\n    clampSize({\n      width: drag.width - (event.clientX - drag.x) * drag.direction,\n      height: drag.height - (event.clientY - drag.y),\n    });\n\n  const takeDrag = (event: PointerEvent<HTMLElement>) => {\n    const drag = dragRef.current;\n    if (!drag || drag.pointerId !== event.pointerId) return null;\n    dragRef.current = null;\n    return drag;\n  };\n\n  const reset = () => {\n    dragRef.current = null;\n    commitSize(null);\n  };\n\n  return {\n    size,\n    reset,\n    handleProps: {\n      onPointerDown: (event: PointerEvent<HTMLElement>) => {\n        const content = contentRef.current;\n        if (event.button !== 0 || !content) return;\n        const rect = content.getBoundingClientRect();\n        dragRef.current = {\n          pointerId: event.pointerId,\n          x: event.clientX,\n          y: event.clientY,\n          width: rect.width,\n          height: rect.height,\n          direction: isRtl(content) ? -1 : 1,\n          last: null,\n        };\n        event.currentTarget.setPointerCapture(event.pointerId);\n        event.preventDefault();\n      },\n      onPointerMove: (event: PointerEvent<HTMLElement>) => {\n        const drag = dragRef.current;\n        if (!drag || drag.pointerId !== event.pointerId) return;\n        if (!drag.last && event.clientX === drag.x && event.clientY === drag.y)\n          return;\n        drag.last = sizeFromPointer(drag, event);\n        setSize(drag.last);\n      },\n      onPointerUp: (event: PointerEvent<HTMLElement>) => {\n        const drag = takeDrag(event);\n        if (!drag) return;\n        if (drag.last || event.clientX !== drag.x || event.clientY !== drag.y) {\n          commitSize(sizeFromPointer(drag, event));\n        }\n      },\n      onPointerCancel: (event: PointerEvent<HTMLElement>) => {\n        const drag = takeDrag(event);\n        if (drag?.last) commitSize(drag.last);\n      },\n      onKeyDown: (event: KeyboardEvent<HTMLElement>) => {\n        const content = contentRef.current;\n        if (!content) return;\n        if (event.key === \"Enter\" || event.key === \" \") {\n          event.preventDefault();\n          reset();\n          return;\n        }\n        const step = event.shiftKey ? RESIZE_STEP * 4 : RESIZE_STEP;\n        const rtl = isRtl(content);\n        const changes: Record<string, readonly [number, number]> = {\n          ArrowUp: [0, step],\n          ArrowDown: [0, -step],\n          [rtl ? \"ArrowRight\" : \"ArrowLeft\"]: [step, 0],\n          [rtl ? \"ArrowLeft\" : \"ArrowRight\"]: [-step, 0],\n        };\n        const change = changes[event.key];\n        if (!change) return;\n        event.preventDefault();\n        const rect = content.getBoundingClientRect();\n        commitSize(\n          clampSize({\n            width: rect.width + change[0],\n            height: rect.height + change[1],\n          }),\n        );\n      },\n    },\n  };\n};\n"
    }
  ]
}