hpo-react-visualizer 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -5,6 +5,11 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
5
5
  * Supported organ identifiers
6
6
  */
7
7
  type OrganId = "head" | "eye" | "ear" | "heart" | "lung" | "digestive" | "kidney" | "integument" | "constitutional" | "limbs" | "nervous" | "breast" | "thoracicCavity" | "voice" | "metabolism" | "cell" | "endocrine" | "neoplasm" | "immune" | "muscle" | "growth" | "prenatal" | "blood";
8
+ /**
9
+ * HPO official category labels
10
+ * @see https://hpo.jax.org/
11
+ */
12
+ type HPOLabel = "others" | "Growth abnormality" | "Abnormality of the genitourinary system" | "Abnormality of the immune system" | "Abnormality of the digestive system" | "Abnormality of metabolism/homeostasis" | "Abnormality of head or neck" | "Abnormality of the musculoskeletal system" | "Abnormality of the nervous system" | "Abnormality of the respiratory system" | "Abnormality of the eye" | "Abnormality of the cardiovascular system" | "Abnormality of the ear" | "Abnormality of prenatal development or birth" | "Abnormality of the integument" | "Abnormality of the breast" | "Abnormality of the endocrine system" | "Abnormality of blood and blood-forming tissues" | "Abnormality of limbs" | "Abnormality of the voice" | "Constitutional symptom" | "Neoplasm" | "Abnormal cellular phenotype" | "Abnormality of the thoracic cavity";
8
13
  /**
9
14
  * Color scheme for each organ.
10
15
  */
@@ -129,6 +134,8 @@ interface HpoVisualizerProps {
129
134
  className?: string;
130
135
  /** Additional inline styles */
131
136
  style?: CSSProperties;
137
+ /** Maximum zoom level (default: 5 = 500%) */
138
+ maxZoom?: number;
132
139
  }
133
140
 
134
141
  /**
@@ -143,6 +150,19 @@ declare const ORGAN_NAMES_EN: Record<OrganId, string>;
143
150
  * Display names for organs (Korean)
144
151
  */
145
152
  declare const ORGAN_NAMES_KO: Record<OrganId, string>;
153
+ /**
154
+ * All HPO category labels
155
+ */
156
+ declare const HPO_LABELS: readonly HPOLabel[];
157
+ /**
158
+ * Mapping from HPO label to OrganId
159
+ * Note: "others" has no corresponding OrganId
160
+ */
161
+ declare const HPO_LABEL_TO_ORGAN: Record<Exclude<HPOLabel, "others">, OrganId>;
162
+ /**
163
+ * Mapping from OrganId to HPO label
164
+ */
165
+ declare const ORGAN_TO_HPO_LABEL: Record<OrganId, Exclude<HPOLabel, "others">>;
146
166
  /**
147
167
  * Color palettes for each color scheme.
148
168
  * Based on Tailwind CSS color palette.
@@ -162,8 +182,9 @@ declare const ANIMATION_DURATION_MS = 150;
162
182
  * - Click to select/deselect organs
163
183
  * - Hover effects work independently even when an organ is selected
164
184
  * - Exposes hover and selection state via callbacks
185
+ * - Zoom and pan support with mouse wheel and drag
165
186
  */
166
- declare function HpoVisualizer({ organs, visibleOrgans, hoveredOrgan: controlledHovered, selectedOrgan: controlledSelected, onHover, onSelect, colorPalette: inputColorPalette, width, height, className, style, }: HpoVisualizerProps): react_jsx_runtime.JSX.Element;
187
+ declare function HpoVisualizer({ organs, visibleOrgans, hoveredOrgan: controlledHovered, selectedOrgan: controlledSelected, onHover, onSelect, colorPalette: inputColorPalette, width, height, className, style, maxZoom, }: HpoVisualizerProps): react_jsx_runtime.JSX.Element;
167
188
 
168
189
  interface OrganSvgWrapperProps {
169
190
  /** Organ identifier */
@@ -241,4 +262,4 @@ interface UseOrganInteractionResult {
241
262
  */
242
263
  declare function useOrganInteraction(options?: UseOrganInteractionOptions): UseOrganInteractionResult;
243
264
 
244
- export { ANIMATION_DURATION_MS, type ColorScale as ColorPalette, type ColorName as ColorScheme, DEFAULT_COLOR_PALETTE, HpoVisualizer, type HpoVisualizerProps, ORGAN_COMPONENTS, ORGAN_IDS, ORGAN_NAMES_EN, ORGAN_NAMES_KO, type OrganConfig, type OrganId, type OrganInteractionHandlers, type OrganInteractionState, type OrganStyle, OrganSvg, type OrganSvgProps, type OrganSvgWrapperProps, type UseOrganInteractionOptions, type UseOrganInteractionResult, useOrganInteraction };
265
+ export { ANIMATION_DURATION_MS, type ColorScale as ColorPalette, type ColorName as ColorScheme, DEFAULT_COLOR_PALETTE, type HPOLabel, HPO_LABELS, HPO_LABEL_TO_ORGAN, HpoVisualizer, type HpoVisualizerProps, ORGAN_COMPONENTS, ORGAN_IDS, ORGAN_NAMES_EN, ORGAN_NAMES_KO, ORGAN_TO_HPO_LABEL, type OrganConfig, type OrganId, type OrganInteractionHandlers, type OrganInteractionState, type OrganStyle, OrganSvg, type OrganSvgProps, type OrganSvgWrapperProps, type UseOrganInteractionOptions, type UseOrganInteractionResult, useOrganInteraction };
package/dist/index.d.ts CHANGED
@@ -5,6 +5,11 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
5
5
  * Supported organ identifiers
6
6
  */
7
7
  type OrganId = "head" | "eye" | "ear" | "heart" | "lung" | "digestive" | "kidney" | "integument" | "constitutional" | "limbs" | "nervous" | "breast" | "thoracicCavity" | "voice" | "metabolism" | "cell" | "endocrine" | "neoplasm" | "immune" | "muscle" | "growth" | "prenatal" | "blood";
8
+ /**
9
+ * HPO official category labels
10
+ * @see https://hpo.jax.org/
11
+ */
12
+ type HPOLabel = "others" | "Growth abnormality" | "Abnormality of the genitourinary system" | "Abnormality of the immune system" | "Abnormality of the digestive system" | "Abnormality of metabolism/homeostasis" | "Abnormality of head or neck" | "Abnormality of the musculoskeletal system" | "Abnormality of the nervous system" | "Abnormality of the respiratory system" | "Abnormality of the eye" | "Abnormality of the cardiovascular system" | "Abnormality of the ear" | "Abnormality of prenatal development or birth" | "Abnormality of the integument" | "Abnormality of the breast" | "Abnormality of the endocrine system" | "Abnormality of blood and blood-forming tissues" | "Abnormality of limbs" | "Abnormality of the voice" | "Constitutional symptom" | "Neoplasm" | "Abnormal cellular phenotype" | "Abnormality of the thoracic cavity";
8
13
  /**
9
14
  * Color scheme for each organ.
10
15
  */
@@ -129,6 +134,8 @@ interface HpoVisualizerProps {
129
134
  className?: string;
130
135
  /** Additional inline styles */
131
136
  style?: CSSProperties;
137
+ /** Maximum zoom level (default: 5 = 500%) */
138
+ maxZoom?: number;
132
139
  }
133
140
 
134
141
  /**
@@ -143,6 +150,19 @@ declare const ORGAN_NAMES_EN: Record<OrganId, string>;
143
150
  * Display names for organs (Korean)
144
151
  */
145
152
  declare const ORGAN_NAMES_KO: Record<OrganId, string>;
153
+ /**
154
+ * All HPO category labels
155
+ */
156
+ declare const HPO_LABELS: readonly HPOLabel[];
157
+ /**
158
+ * Mapping from HPO label to OrganId
159
+ * Note: "others" has no corresponding OrganId
160
+ */
161
+ declare const HPO_LABEL_TO_ORGAN: Record<Exclude<HPOLabel, "others">, OrganId>;
162
+ /**
163
+ * Mapping from OrganId to HPO label
164
+ */
165
+ declare const ORGAN_TO_HPO_LABEL: Record<OrganId, Exclude<HPOLabel, "others">>;
146
166
  /**
147
167
  * Color palettes for each color scheme.
148
168
  * Based on Tailwind CSS color palette.
@@ -162,8 +182,9 @@ declare const ANIMATION_DURATION_MS = 150;
162
182
  * - Click to select/deselect organs
163
183
  * - Hover effects work independently even when an organ is selected
164
184
  * - Exposes hover and selection state via callbacks
185
+ * - Zoom and pan support with mouse wheel and drag
165
186
  */
166
- declare function HpoVisualizer({ organs, visibleOrgans, hoveredOrgan: controlledHovered, selectedOrgan: controlledSelected, onHover, onSelect, colorPalette: inputColorPalette, width, height, className, style, }: HpoVisualizerProps): react_jsx_runtime.JSX.Element;
187
+ declare function HpoVisualizer({ organs, visibleOrgans, hoveredOrgan: controlledHovered, selectedOrgan: controlledSelected, onHover, onSelect, colorPalette: inputColorPalette, width, height, className, style, maxZoom, }: HpoVisualizerProps): react_jsx_runtime.JSX.Element;
167
188
 
168
189
  interface OrganSvgWrapperProps {
169
190
  /** Organ identifier */
@@ -241,4 +262,4 @@ interface UseOrganInteractionResult {
241
262
  */
242
263
  declare function useOrganInteraction(options?: UseOrganInteractionOptions): UseOrganInteractionResult;
243
264
 
244
- export { ANIMATION_DURATION_MS, type ColorScale as ColorPalette, type ColorName as ColorScheme, DEFAULT_COLOR_PALETTE, HpoVisualizer, type HpoVisualizerProps, ORGAN_COMPONENTS, ORGAN_IDS, ORGAN_NAMES_EN, ORGAN_NAMES_KO, type OrganConfig, type OrganId, type OrganInteractionHandlers, type OrganInteractionState, type OrganStyle, OrganSvg, type OrganSvgProps, type OrganSvgWrapperProps, type UseOrganInteractionOptions, type UseOrganInteractionResult, useOrganInteraction };
265
+ export { ANIMATION_DURATION_MS, type ColorScale as ColorPalette, type ColorName as ColorScheme, DEFAULT_COLOR_PALETTE, type HPOLabel, HPO_LABELS, HPO_LABEL_TO_ORGAN, HpoVisualizer, type HpoVisualizerProps, ORGAN_COMPONENTS, ORGAN_IDS, ORGAN_NAMES_EN, ORGAN_NAMES_KO, ORGAN_TO_HPO_LABEL, type OrganConfig, type OrganId, type OrganInteractionHandlers, type OrganInteractionState, type OrganStyle, OrganSvg, type OrganSvgProps, type OrganSvgWrapperProps, type UseOrganInteractionOptions, type UseOrganInteractionResult, useOrganInteraction };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use client";
2
- import { useMemo, useState, useCallback, useId } from 'react';
3
- import { jsx, jsxs } from 'react/jsx-runtime';
2
+ import { useMemo, useState, useCallback, useId, useRef, useEffect } from 'react';
3
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
4
 
5
5
  // src/constants.ts
6
6
  var ORGAN_IDS = [
@@ -78,6 +78,82 @@ var ORGAN_NAMES_KO = {
78
78
  prenatal: "\uD0DC\uC544",
79
79
  blood: "\uD608\uC561"
80
80
  };
81
+ var HPO_LABELS = [
82
+ "others",
83
+ "Growth abnormality",
84
+ "Abnormality of the genitourinary system",
85
+ "Abnormality of the immune system",
86
+ "Abnormality of the digestive system",
87
+ "Abnormality of metabolism/homeostasis",
88
+ "Abnormality of head or neck",
89
+ "Abnormality of the musculoskeletal system",
90
+ "Abnormality of the nervous system",
91
+ "Abnormality of the respiratory system",
92
+ "Abnormality of the eye",
93
+ "Abnormality of the cardiovascular system",
94
+ "Abnormality of the ear",
95
+ "Abnormality of prenatal development or birth",
96
+ "Abnormality of the integument",
97
+ "Abnormality of the breast",
98
+ "Abnormality of the endocrine system",
99
+ "Abnormality of blood and blood-forming tissues",
100
+ "Abnormality of limbs",
101
+ "Abnormality of the voice",
102
+ "Constitutional symptom",
103
+ "Neoplasm",
104
+ "Abnormal cellular phenotype",
105
+ "Abnormality of the thoracic cavity"
106
+ ];
107
+ var HPO_LABEL_TO_ORGAN = {
108
+ "Growth abnormality": "growth",
109
+ "Abnormality of the genitourinary system": "kidney",
110
+ "Abnormality of the immune system": "immune",
111
+ "Abnormality of the digestive system": "digestive",
112
+ "Abnormality of metabolism/homeostasis": "metabolism",
113
+ "Abnormality of head or neck": "head",
114
+ "Abnormality of the musculoskeletal system": "muscle",
115
+ "Abnormality of the nervous system": "nervous",
116
+ "Abnormality of the respiratory system": "lung",
117
+ "Abnormality of the eye": "eye",
118
+ "Abnormality of the cardiovascular system": "heart",
119
+ "Abnormality of the ear": "ear",
120
+ "Abnormality of prenatal development or birth": "prenatal",
121
+ "Abnormality of the integument": "integument",
122
+ "Abnormality of the breast": "breast",
123
+ "Abnormality of the endocrine system": "endocrine",
124
+ "Abnormality of blood and blood-forming tissues": "blood",
125
+ "Abnormality of limbs": "limbs",
126
+ "Abnormality of the voice": "voice",
127
+ "Constitutional symptom": "constitutional",
128
+ Neoplasm: "neoplasm",
129
+ "Abnormal cellular phenotype": "cell",
130
+ "Abnormality of the thoracic cavity": "thoracicCavity"
131
+ };
132
+ var ORGAN_TO_HPO_LABEL = {
133
+ growth: "Growth abnormality",
134
+ kidney: "Abnormality of the genitourinary system",
135
+ immune: "Abnormality of the immune system",
136
+ digestive: "Abnormality of the digestive system",
137
+ metabolism: "Abnormality of metabolism/homeostasis",
138
+ head: "Abnormality of head or neck",
139
+ muscle: "Abnormality of the musculoskeletal system",
140
+ nervous: "Abnormality of the nervous system",
141
+ lung: "Abnormality of the respiratory system",
142
+ eye: "Abnormality of the eye",
143
+ heart: "Abnormality of the cardiovascular system",
144
+ ear: "Abnormality of the ear",
145
+ prenatal: "Abnormality of prenatal development or birth",
146
+ integument: "Abnormality of the integument",
147
+ breast: "Abnormality of the breast",
148
+ endocrine: "Abnormality of the endocrine system",
149
+ blood: "Abnormality of blood and blood-forming tissues",
150
+ limbs: "Abnormality of limbs",
151
+ voice: "Abnormality of the voice",
152
+ constitutional: "Constitutional symptom",
153
+ neoplasm: "Neoplasm",
154
+ cell: "Abnormal cellular phenotype",
155
+ thoracicCavity: "Abnormality of the thoracic cavity"
156
+ };
81
157
  var DEFAULT_COLOR_PALETTE = {
82
158
  blue: {
83
159
  100: "#DBEAFE",
@@ -1301,6 +1377,211 @@ function useOrganInteraction(options = {}) {
1301
1377
  isHovered
1302
1378
  };
1303
1379
  }
1380
+ function useZoom(options = {}) {
1381
+ const { minZoom = 1, maxZoom = 5, zoomStep = 0.5 } = options;
1382
+ const [zoom, setZoom] = useState(1);
1383
+ const [pan, setPan] = useState({ x: 0, y: 0 });
1384
+ const [isDragging, setIsDragging] = useState(false);
1385
+ const dragStartRef = useRef({ x: 0, y: 0 });
1386
+ const panStartRef = useRef({ x: 0, y: 0 });
1387
+ const containerRef = useRef(null);
1388
+ const clampZoom = useCallback(
1389
+ (value) => Math.max(minZoom, Math.min(maxZoom, value)),
1390
+ [minZoom, maxZoom]
1391
+ );
1392
+ const zoomIn = useCallback(() => {
1393
+ setZoom((prev) => clampZoom(prev + zoomStep));
1394
+ }, [clampZoom, zoomStep]);
1395
+ const zoomOut = useCallback(() => {
1396
+ setZoom((prev) => clampZoom(prev - zoomStep));
1397
+ }, [clampZoom, zoomStep]);
1398
+ const resetZoom = useCallback(() => {
1399
+ setZoom(1);
1400
+ setPan({ x: 0, y: 0 });
1401
+ }, []);
1402
+ useEffect(() => {
1403
+ const container = containerRef.current;
1404
+ if (!container) return;
1405
+ const wheelZoomStep = 0.1;
1406
+ const handleWheel = (e) => {
1407
+ e.preventDefault();
1408
+ const rect = container.getBoundingClientRect();
1409
+ const mouseX = e.clientX - rect.left - rect.width / 2;
1410
+ const mouseY = e.clientY - rect.top - rect.height / 2;
1411
+ const delta = e.deltaY > 0 ? -wheelZoomStep : wheelZoomStep;
1412
+ setZoom((prevZoom) => {
1413
+ const newZoom = clampZoom(prevZoom + delta);
1414
+ if (newZoom === prevZoom) return prevZoom;
1415
+ setPan((prevPan) => {
1416
+ const contentX = (mouseX - prevPan.x) / prevZoom;
1417
+ const contentY = (mouseY - prevPan.y) / prevZoom;
1418
+ const newPanX = mouseX - contentX * newZoom;
1419
+ const newPanY = mouseY - contentY * newZoom;
1420
+ if (newZoom <= 1) {
1421
+ return { x: 0, y: 0 };
1422
+ }
1423
+ const maxPanX = rect.width * (newZoom - 1) / 2;
1424
+ const maxPanY = rect.height * (newZoom - 1) / 2;
1425
+ return {
1426
+ x: Math.max(-maxPanX, Math.min(maxPanX, newPanX)),
1427
+ y: Math.max(-maxPanY, Math.min(maxPanY, newPanY))
1428
+ };
1429
+ });
1430
+ return newZoom;
1431
+ });
1432
+ };
1433
+ container.addEventListener("wheel", handleWheel, { passive: false });
1434
+ return () => {
1435
+ container.removeEventListener("wheel", handleWheel);
1436
+ };
1437
+ }, [clampZoom]);
1438
+ const clampPan = useCallback((newPan, currentZoom) => {
1439
+ const container = containerRef.current;
1440
+ if (!container || currentZoom <= 1) {
1441
+ return { x: 0, y: 0 };
1442
+ }
1443
+ const containerWidth = container.clientWidth;
1444
+ const containerHeight = container.clientHeight;
1445
+ const maxPanX = containerWidth * (currentZoom - 1) / 2;
1446
+ const maxPanY = containerHeight * (currentZoom - 1) / 2;
1447
+ return {
1448
+ x: Math.max(-maxPanX, Math.min(maxPanX, newPan.x)),
1449
+ y: Math.max(-maxPanY, Math.min(maxPanY, newPan.y))
1450
+ };
1451
+ }, []);
1452
+ useEffect(() => {
1453
+ setPan((currentPan) => clampPan(currentPan, zoom));
1454
+ }, [zoom, clampPan]);
1455
+ const handleMouseDown = useCallback(
1456
+ (e) => {
1457
+ if (e.button !== 0 || zoom <= 1) return;
1458
+ setIsDragging(true);
1459
+ dragStartRef.current = { x: e.clientX, y: e.clientY };
1460
+ panStartRef.current = { ...pan };
1461
+ e.preventDefault();
1462
+ },
1463
+ [pan, zoom]
1464
+ );
1465
+ const handleMouseMove = useCallback(
1466
+ (e) => {
1467
+ if (!isDragging || zoom <= 1) return;
1468
+ const dx = e.clientX - dragStartRef.current.x;
1469
+ const dy = e.clientY - dragStartRef.current.y;
1470
+ const newPan = {
1471
+ x: panStartRef.current.x + dx,
1472
+ y: panStartRef.current.y + dy
1473
+ };
1474
+ setPan(clampPan(newPan, zoom));
1475
+ },
1476
+ [isDragging, zoom, clampPan]
1477
+ );
1478
+ const handleMouseUp = useCallback(() => {
1479
+ setIsDragging(false);
1480
+ }, []);
1481
+ const isDefaultZoom = zoom === 1 && pan.x === 0 && pan.y === 0;
1482
+ return {
1483
+ zoom,
1484
+ pan,
1485
+ zoomIn,
1486
+ zoomOut,
1487
+ resetZoom,
1488
+ handleMouseDown,
1489
+ handleMouseMove,
1490
+ handleMouseUp,
1491
+ isDragging,
1492
+ isDefaultZoom,
1493
+ containerRef
1494
+ };
1495
+ }
1496
+ var buttonBaseStyle = {
1497
+ width: 32,
1498
+ height: 32,
1499
+ display: "flex",
1500
+ alignItems: "center",
1501
+ justifyContent: "center",
1502
+ backgroundColor: "rgba(255, 255, 255, 0.9)",
1503
+ border: "1px solid #d1d5db",
1504
+ borderRadius: 6,
1505
+ cursor: "pointer",
1506
+ fontSize: 18,
1507
+ fontWeight: "bold",
1508
+ color: "#374151",
1509
+ transition: "background-color 0.15s ease",
1510
+ userSelect: "none"
1511
+ };
1512
+ var buttonDisabledStyle = {
1513
+ ...buttonBaseStyle,
1514
+ opacity: 0.4,
1515
+ cursor: "not-allowed"
1516
+ };
1517
+ var getContainerStyle = (isVisible) => ({
1518
+ position: "absolute",
1519
+ bottom: 12,
1520
+ right: 12,
1521
+ display: "flex",
1522
+ flexDirection: "column",
1523
+ gap: 4,
1524
+ zIndex: 10,
1525
+ opacity: isVisible ? 1 : 0,
1526
+ transition: "opacity 0.2s ease-in-out",
1527
+ pointerEvents: isVisible ? "auto" : "none"
1528
+ });
1529
+ var getResetContainerStyle = (isVisible) => ({
1530
+ position: "absolute",
1531
+ bottom: 12,
1532
+ left: 12,
1533
+ zIndex: 10,
1534
+ opacity: isVisible ? 1 : 0,
1535
+ transition: "opacity 0.2s ease-in-out",
1536
+ pointerEvents: isVisible ? "auto" : "none"
1537
+ });
1538
+ var resetButtonStyle = {
1539
+ ...buttonBaseStyle,
1540
+ width: "auto",
1541
+ padding: "6px 12px",
1542
+ fontSize: 12,
1543
+ fontWeight: 500
1544
+ };
1545
+ function ZoomControls({
1546
+ onZoomIn,
1547
+ onZoomOut,
1548
+ onReset,
1549
+ showResetButton,
1550
+ zoom,
1551
+ minZoom,
1552
+ maxZoom,
1553
+ isVisible
1554
+ }) {
1555
+ const isMinZoom = zoom <= minZoom;
1556
+ const isMaxZoom = zoom >= maxZoom;
1557
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1558
+ /* @__PURE__ */ jsxs("div", { style: getContainerStyle(isVisible), children: [
1559
+ /* @__PURE__ */ jsx(
1560
+ "button",
1561
+ {
1562
+ type: "button",
1563
+ onClick: onZoomIn,
1564
+ disabled: isMaxZoom,
1565
+ style: isMaxZoom ? buttonDisabledStyle : buttonBaseStyle,
1566
+ "aria-label": "Zoom in",
1567
+ children: "+"
1568
+ }
1569
+ ),
1570
+ /* @__PURE__ */ jsx(
1571
+ "button",
1572
+ {
1573
+ type: "button",
1574
+ onClick: onZoomOut,
1575
+ disabled: isMinZoom,
1576
+ style: isMinZoom ? buttonDisabledStyle : buttonBaseStyle,
1577
+ "aria-label": "Zoom out",
1578
+ children: "\u2212"
1579
+ }
1580
+ )
1581
+ ] }),
1582
+ /* @__PURE__ */ jsx("div", { style: getResetContainerStyle(isVisible && showResetButton), children: /* @__PURE__ */ jsx("button", { type: "button", onClick: onReset, style: resetButtonStyle, "aria-label": "Reset zoom", children: "\u21BA" }) })
1583
+ ] });
1584
+ }
1304
1585
  var ORGAN_POSITIONS = {
1305
1586
  growth: { x: 104, y: 1, width: 12, height: 349, viewBox: "0 0 12 349" },
1306
1587
  constitutional: { x: 0, y: 0, width: 122, height: 358, viewBox: "0 0 122 358" },
@@ -1351,6 +1632,7 @@ var FOREGROUND_ORGANS = [
1351
1632
  "prenatal",
1352
1633
  "blood"
1353
1634
  ];
1635
+ var MIN_ZOOM = 1;
1354
1636
  function HpoVisualizer({
1355
1637
  organs,
1356
1638
  visibleOrgans,
@@ -1362,9 +1644,24 @@ function HpoVisualizer({
1362
1644
  width = BODY_VIEWBOX.width,
1363
1645
  height = BODY_VIEWBOX.height,
1364
1646
  className,
1365
- style
1647
+ style,
1648
+ maxZoom = 5
1366
1649
  }) {
1367
1650
  const visualizerID = useId();
1651
+ const [isHovering, setIsHovering] = useState(false);
1652
+ const {
1653
+ zoom,
1654
+ pan,
1655
+ zoomIn,
1656
+ zoomOut,
1657
+ resetZoom,
1658
+ handleMouseDown,
1659
+ handleMouseMove,
1660
+ handleMouseUp,
1661
+ isDragging,
1662
+ isDefaultZoom,
1663
+ containerRef
1664
+ } = useZoom({ minZoom: MIN_ZOOM, maxZoom });
1368
1665
  const colorPalette = useMemo(
1369
1666
  () => createStrictColorPalette(inputColorPalette),
1370
1667
  [inputColorPalette]
@@ -1401,7 +1698,19 @@ function HpoVisualizer({
1401
1698
  position: "relative",
1402
1699
  width,
1403
1700
  height,
1404
- ...style
1701
+ ...style,
1702
+ // Apply overflow after style spread to ensure it takes precedence
1703
+ // Use 'clip' instead of 'hidden' to respect padding
1704
+ overflow: "clip"
1705
+ };
1706
+ const contentStyle = {
1707
+ position: "relative",
1708
+ width: "100%",
1709
+ height: "100%",
1710
+ transform: `scale(${zoom}) translate(${pan.x / zoom}px, ${pan.y / zoom}px)`,
1711
+ transformOrigin: "center center",
1712
+ cursor: isDragging ? "grabbing" : zoom > 1 ? "grab" : "default",
1713
+ transition: isDragging ? "none" : "transform 0.1s ease-out"
1405
1714
  };
1406
1715
  const viewBox = `0 0 ${BODY_VIEWBOX.width} ${BODY_VIEWBOX.height}`;
1407
1716
  const scale = Math.min(Number(width) / BODY_VIEWBOX.width, Number(height) / BODY_VIEWBOX.height);
@@ -1438,36 +1747,73 @@ function HpoVisualizer({
1438
1747
  `${visualizerID}-${organId}`
1439
1748
  );
1440
1749
  };
1441
- return /* @__PURE__ */ jsxs("div", { className, style: containerStyle, children: [
1442
- BACKGROUND_ORGANS.map((organId) => renderOrgan(organId, visibleOrganIds.includes(organId))),
1443
- /* @__PURE__ */ jsxs(
1444
- "svg",
1445
- {
1446
- x: translateX,
1447
- y: "0",
1448
- width,
1449
- height,
1450
- viewBox,
1451
- style: { position: "absolute", top: "0", left: "0" },
1452
- pointerEvents: "none",
1453
- children: [
1454
- /* @__PURE__ */ jsx("title", { children: "Human body" }),
1455
- /* @__PURE__ */ jsx(
1456
- Body,
1750
+ return /* @__PURE__ */ jsxs(
1751
+ "div",
1752
+ {
1753
+ ref: containerRef,
1754
+ className,
1755
+ style: containerStyle,
1756
+ onMouseEnter: () => setIsHovering(true),
1757
+ onMouseLeave: () => {
1758
+ setIsHovering(false);
1759
+ handleMouseUp();
1760
+ },
1761
+ onMouseDown: handleMouseDown,
1762
+ onMouseMove: handleMouseMove,
1763
+ onMouseUp: handleMouseUp,
1764
+ role: "application",
1765
+ tabIndex: 0,
1766
+ children: [
1767
+ /* @__PURE__ */ jsxs("div", { style: contentStyle, children: [
1768
+ BACKGROUND_ORGANS.map(
1769
+ (organId) => renderOrgan(organId, visibleOrganIds.includes(organId))
1770
+ ),
1771
+ /* @__PURE__ */ jsxs(
1772
+ "svg",
1457
1773
  {
1458
- colorScale: colorPalette[DEFAULT_COLOR_NAME],
1459
- style: {
1460
- fill: "#fff"
1461
- }
1774
+ x: translateX,
1775
+ y: "0",
1776
+ width,
1777
+ height,
1778
+ viewBox,
1779
+ style: { position: "absolute", top: "0", left: "0" },
1780
+ pointerEvents: "none",
1781
+ children: [
1782
+ /* @__PURE__ */ jsx("title", { children: "Human body" }),
1783
+ /* @__PURE__ */ jsx(
1784
+ Body,
1785
+ {
1786
+ colorScale: colorPalette[DEFAULT_COLOR_NAME],
1787
+ style: {
1788
+ fill: "#fff"
1789
+ }
1790
+ }
1791
+ )
1792
+ ]
1462
1793
  }
1794
+ ),
1795
+ FOREGROUND_ORGANS.map(
1796
+ (organId) => renderOrgan(organId, visibleOrganIds.includes(organId))
1463
1797
  )
1464
- ]
1465
- }
1466
- ),
1467
- FOREGROUND_ORGANS.map((organId) => renderOrgan(organId, visibleOrganIds.includes(organId)))
1468
- ] });
1798
+ ] }),
1799
+ /* @__PURE__ */ jsx(
1800
+ ZoomControls,
1801
+ {
1802
+ onZoomIn: zoomIn,
1803
+ onZoomOut: zoomOut,
1804
+ onReset: resetZoom,
1805
+ showResetButton: !isDefaultZoom,
1806
+ zoom,
1807
+ minZoom: MIN_ZOOM,
1808
+ maxZoom,
1809
+ isVisible: isHovering
1810
+ }
1811
+ )
1812
+ ]
1813
+ }
1814
+ );
1469
1815
  }
1470
1816
 
1471
- export { ANIMATION_DURATION_MS, DEFAULT_COLOR_PALETTE, HpoVisualizer, ORGAN_COMPONENTS, ORGAN_IDS, ORGAN_NAMES_EN, ORGAN_NAMES_KO, OrganSvg, useOrganInteraction };
1817
+ export { ANIMATION_DURATION_MS, DEFAULT_COLOR_PALETTE, HPO_LABELS, HPO_LABEL_TO_ORGAN, HpoVisualizer, ORGAN_COMPONENTS, ORGAN_IDS, ORGAN_NAMES_EN, ORGAN_NAMES_KO, ORGAN_TO_HPO_LABEL, OrganSvg, useOrganInteraction };
1472
1818
  //# sourceMappingURL=index.js.map
1473
1819
  //# sourceMappingURL=index.js.map