создай на react рабочую область в виджетами, с возможностью их перемещать на рабочую область и масштабировать

Вот пример:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Виджеты Drag & Drop</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
<!-- FontAwesome -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet" />
<!-- interact.js -->
<script src="https://cdn.jsdelivr.net/npm/interactjs/dist/interact.min.js"></script>
<style>
  body, html {
    height: 100%;
    margin: 0;
    background: #f8f9fa;
    font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
  }
  .workspace {
    position: relative;
    height: calc(100vh - 80px);
    background: #fff;
    border-bottom: 1px solid #dee2e6;
    overflow: hidden;
  }
  .widget {
    position: absolute;
    background: #ffffff;
    border: 1px solid #ddd;
    border-radius: 8px;
    box-shadow: 0 2px 6px rgb(0 0 0 / 0.1);
    padding: 10px 10px 30px 10px;
    user-select: none;
    overflow: hidden;
    min-width: 150px;
    min-height: 100px;
    resize: both;
  }
  .widget-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    cursor: move;
    font-weight: 600;
    font-size: 1rem;
    border-bottom: 1px solid #eee;
    padding-bottom: 5px;
    margin-bottom: 10px;
  }
  .widget-header .btn-close {
    font-size: 0.8rem;
    padding: 0;
    line-height: 1;
  }

  .bottom-panel {
    height: 80px;
    background: #212529;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 1rem;
    padding: 0 15px;
  }
  .widget-thumb {
    width: 60px;
    height: 60px;
    background: #343a40;
    border-radius: 10px;
    color: #fff;
    font-size: 0.9rem;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    user-select: none;
    cursor: grab;
    transition: background-color 0.3s, box-shadow 0.3s;
    border: 2px solid transparent;
  }
  .widget-thumb.dragging {
    cursor: grabbing;
    opacity: 0.7;
  }
  .widget-thumb.active {
    border-color: #0d6efd;
    background-color: #0d6efd;
    box-shadow: 0 0 10px #0d6efd88;
  }
  .widget-thumb i {
    font-size: 1.5rem;
    margin-bottom: 4px;
  }

  /* Scroll horizontal for bottom panel if widgets overflow */
  .bottom-panel {
    overflow-x: auto;
  }
  
  /* No scroll bar */
  .bottom-panel::-webkit-scrollbar {
    display: none;
  }
  .bottom-panel {
    -ms-overflow-style: none;  /* IE and Edge */
    scrollbar-width: none;  /* Firefox */
  }
</style>
</head>
<body>

<div class="workspace" id="workspace"></div>

<div class="bottom-panel" id="bottomPanel" aria-label="Панель виджетов" role="list">
  <!-- Виджеты будут тут -->
</div>

<script>
  // Конфигурация виджетов
  // id - уникальный идентификатор (string)
  // name - название виджета
  // icon - иконка FontAwesome
  // content - пример содержимого
  const widgetList = [
    {id: 'weather', name: 'Погода', icon: 'fa-cloud-sun', content: '<p>Прогноз погоды: <strong>Солнечно, 25°C</strong></p>'},
    {id: 'news', name: 'Новости', icon: 'fa-newspaper', content: '<ul><li>Новости 1</li><li>Новости 2</li></ul>'},
    {id: 'clock', name: 'Часы', icon: 'fa-clock', content: '<div id="clockDisplay" style="font-size:1.2rem; text-align:center;"></div>'},
    {id: 'stocks', name: 'Акции', icon: 'fa-chart-line', content: '<p>Акции: <strong>+1.2%</strong></p>'},
    {id: 'todo', name: 'Задачи', icon: 'fa-list-check', content: '<ul><li>Сделать отчёт</li><li>Позвонить клиенту</li></ul>'},
  ];

  const workspace = document.getElementById('workspace');
  const bottomPanel = document.getElementById('bottomPanel');

  // Отслеживаем добавленные виджеты {widgetId: true}
  const addedWidgets = {};

  // Создаем панель виджетов внизу
  function initBottomPanel() {
    widgetList.forEach(w => {
      const thumb = document.createElement('div');
      thumb.className = 'widget-thumb';
      thumb.dataset.widgetId = w.id;
      thumb.tabIndex = 0; // фокусируемый
      thumb.setAttribute('role', 'listitem');
      thumb.setAttribute('aria-label', w.name);

      thumb.innerHTML = `<i class="fa-solid ${w.icon}"></i><span>${w.name}</span>`;
      bottomPanel.appendChild(thumb);

      // Drag start для перетаскивания на workspace
      thumb.addEventListener('mousedown', (e) => {
        startDragNewWidget(e, w);
      });
      thumb.addEventListener('touchstart', (e) => {
        startDragNewWidget(e, w);
      });
      // Можно также добавить keyboard support для добавления по Enter/Space
      thumb.addEventListener('keydown', (e) => {
        if(e.key === 'Enter' || e.key === ' ') {
          addWidgetToWorkspace(w);
          e.preventDefault();
        }
      });
    });
  }

  // Функция для перетаскивания нового виджета с панели на рабочий экран
  let dragNewWidgetElem = null;
  let dragNewWidgetData = null;

  function startDragNewWidget(e, widgetData) {
    e.preventDefault();
    dragNewWidgetData = widgetData;
    dragNewWidgetElem = document.createElement('div');
    dragNewWidgetElem.className = 'widget widget-dragging';
    dragNewWidgetElem.style.position = 'fixed';
    dragNewWidgetElem.style.pointerEvents = 'none';
    dragNewWidgetElem.style.zIndex = 9999;
    dragNewWidgetElem.style.width = '160px';
    dragNewWidgetElem.style.height = '120px';
    dragNewWidgetElem.style.background = '#0d6efd33';
    dragNewWidgetElem.style.border = '2px dashed #0d6efd';
    dragNewWidgetElem.style.borderRadius = '8px';
    dragNewWidgetElem.style.display = 'flex';
    dragNewWidgetElem.style.alignItems = 'center';
    dragNewWidgetElem.style.justifyContent = 'center';
    dragNewWidgetElem.style.color = '#0d6efd';
    dragNewWidgetElem.style.fontWeight = '600';
    dragNewWidgetElem.style.userSelect = 'none';
    dragNewWidgetElem.innerText = widgetData.name;

    document.body.appendChild(dragNewWidgetElem);

    moveDragNewWidgetElem(e);

    window.addEventListener('mousemove', moveDragNewWidgetElem);
    window.addEventListener('touchmove', moveDragNewWidgetElem);
    window.addEventListener('mouseup', dropDragNewWidgetElem);
    window.addEventListener('touchend', dropDragNewWidgetElem);
  }

  function moveDragNewWidgetElem(e) {
    let clientX, clientY;
    if(e.touches) {
      clientX = e.touches[0].clientX;
      clientY = e.touches[0].clientY;
    } else {
      clientX = e.clientX;
      clientY = e.clientY;
    }
    dragNewWidgetElem.style.left = (clientX - dragNewWidgetElem.offsetWidth/2) + 'px';
    dragNewWidgetElem.style.top = (clientY - dragNewWidgetElem.offsetHeight/2) + 'px';
  }

  function dropDragNewWidgetElem(e) {
    // Проверяем, попали ли в workspace
    let clientX, clientY;
    if(e.changedTouches) {
      clientX = e.changedTouches[0].clientX;
      clientY = e.changedTouches[0].clientY;
    } else {
      clientX = e.clientX;
      clientY = e.clientY;
    }
    const rect = workspace.getBoundingClientRect();
    if(clientX >= rect.left && clientX <= rect.right &&
       clientY >= rect.top && clientY <= rect.bottom) {
      const x = clientX - rect.left;
      const y = clientY - rect.top;
      addWidgetToWorkspace(dragNewWidgetData, x, y);
    }
    cleanupDragNewWidget();
  }

  function cleanupDragNewWidget() {
    if(dragNewWidgetElem) {
      document.body.removeChild(dragNewWidgetElem);
      dragNewWidgetElem = null;
      dragNewWidgetData = null;
    }
    window.removeEventListener('mousemove', moveDragNewWidgetElem);
    window.removeEventListener('touchmove', moveDragNewWidgetElem);
    window.removeEventListener('mouseup', dropDragNewWidgetElem);
    window.removeEventListener('touchend', dropDragNewWidgetElem);
  }

  // Добавляем виджет на рабочий экран
  function addWidgetToWorkspace(widgetData, x=50, y=50) {
    // Проверим, если виджет уже добавлен, то не добавляем дубликат
    if(addedWidgets[widgetData.id]) {
      return;
    }
    addedWidgets[widgetData.id] = true;
    updatePanelActiveStates();

    const w = document.createElement('div');
    w.className = 'widget';
    w.dataset.widgetId = widgetData.id;
    w.style.left = x + 'px';
    w.style.top = y+ 'px';
    w.style.width = '200px';
    w.style.height = '150px';

    w.innerHTML = `
      <div class="widget-header" tabindex="0" aria-label="Заголовок виджета: ${widgetData.name}">
        <span class="widget-title">${widgetData.name}</span>
        <button type="button" class="btn-close" aria-label="Удалить ${widgetData.name}"></button>
      </div>
      <div class="widget-body">${widgetData.content}</div>
    `;

    workspace.appendChild(w);

    // Enable draggable and resizable by interact.js:
    interact(w)
      .draggable({
        allowFrom: '.widget-header',
        inertia: true,
        onmove: dragMoveListener,
        modifiers: [
          interact.modifiers.restrictRect({
            restriction: workspace,
            endOnly: true
          })
        ]
      })
      .resizable({
        edges: { left:true, right:true, bottom:true, top:true },
        inertia: true,
        modifiers: [
          interact.modifiers.restrictEdges({
            outer: workspace,
            endOnly: true
          }),
          interact.modifiers.restrictSize({
            min: { width: 150, height: 100 },
            max: { width: workspace.clientWidth, height: workspace.clientHeight }
          })
        ],
        onmove: resizeMoveListener
      });
    
    // Обновление времени в clock (если это часы)
    if(widgetData.id === 'clock') {
      startClock(w.querySelector('#clockDisplay'));
    }

    // Удаление виджета
    const closeBtn = w.querySelector('.btn-close');
    closeBtn.addEventListener('click', () => {
      workspace.removeChild(w);
      delete addedWidgets[widgetData.id];
      updatePanelActiveStates();
    });
  }

  // Обновить подсветку в панели виджетов
  function updatePanelActiveStates() {
    document.querySelectorAll('.widget-thumb').forEach(thumb => {
      const widgetId = thumb.dataset.widgetId;
      if(addedWidgets[widgetId]) {
        thumb.classList.add('active');
      } else {
        thumb.classList.remove('active');
      }
    });
  }

  // Слушатель движения при перетаскивании виджета на рабочем поле
  function dragMoveListener(event) {
    const target = event.target;
    // сохраняем текущие координаты
    const x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx;
    const y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy;

    target.style.webkitTransform =
    target.style.transform =
      'translate(' + x + 'px, ' + y + 'px)';

    target.setAttribute('data-x', x);
    target.setAttribute('data-y', y);
  }

  // Слушатель изменения размера
  function resizeMoveListener(event) {
    const target = event.target;
    let x = (parseFloat(target.getAttribute('data-x')) || 0);
    let y = (parseFloat(target.getAttribute('data-y')) || 0);

    // обновляем размер
    target.style.width = event.rect.width + 'px';
    target.style.height = event.rect.height + 'px';

    // возможно смещается верхний или левый край 
    x += event.deltaRect.left;
    y += event.deltaRect.top;

    target.style.webkitTransform =
    target.style.transform =
      'translate(' + x + 'px,' + y + 'px)';

    target.setAttribute('data-x', x);
    target.setAttribute('data-y', y);
  }

  // Запускаем часы обновления для виджета часов
  function startClock(container) {
    function updateTime() {
      if(!container) return;
      const now = new Date();
      container.textContent = now.toLocaleTimeString();
    }
    updateTime();
    setInterval(updateTime, 1000);
  }

  // Инициализация
  initBottomPanel();
</script>

</body>
</html>

Помимо этого, надо еще:
- чтобы виджеты не могли никак выйти за пределы рабочей области
- чтобы все виджеты были наследниками класс виджет
- чтобы в app.js все выглядело как например
const App = () => (
  <WidgetsContainer>
    <ChartWidget name="График" icon="fas fa-chart-line" />
    <TableWidget name="Таблица" icon="fas fa-table" />
  </WidgetsContainer>
);

export default App;

вынеси стили в index.css