vit-design-webapp / src / lib / hooks / useKeyboardShortcuts.ts Blame
268 lines
65beb53 samthecodingguy Jan 29, 2026
'use client';
import { useEffect, useCallback } from 'react';
import { useEditorStore } from '@/lib/store/editor-store';
import type { LayerData, LayerType } from '@/types';
interface ClipboardData {
  layers: Array<{
    type: LayerType;
    name: string;
    size: { width: number; height: number };
    data: LayerData;
    opacity: number;
    rotation: number;
  }>;
}
// Store clipboard data in memory (browser clipboard API can't handle complex objects)
let clipboardData: ClipboardData | null = null;
export function useKeyboardShortcuts() {
  const {
    selectedLayerIds,
    layers,
    deleteLayer,
    duplicateLayer,
    updateLayer,
    addLayer,
    selectLayer,
    deselectAll,
    undo,
    redo,
    canvas,
    setZoom,
    saveToHistory,
  } = useEditorStore();
  // Check if user is typing in an input
  const checkInputFocus = useCallback(() => {
    const activeElement = document.activeElement;
    const tagName = activeElement?.tagName.toLowerCase();
    return tagName === 'input' || tagName === 'textarea' || tagName === 'select';
  }, []);
  const handleKeyDown = useCallback(
    (e: KeyboardEvent) => {
      // Don't interfere with input fields
      if (checkInputFocus()) return;
      const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
      const cmdKey = isMac ? e.metaKey : e.ctrlKey;
      // Delete selected layers
      if ((e.key === 'Delete' || e.key === 'Backspace') && !cmdKey) {
        e.preventDefault();
        selectedLayerIds.forEach((id) => deleteLayer(id));
        return;
      }
      // Undo
      if (cmdKey && e.key === 'z' && !e.shiftKey) {
        e.preventDefault();
        undo();
        return;
      }
      // Redo
      if (cmdKey && e.key === 'z' && e.shiftKey) {
        e.preventDefault();
        redo();
        return;
      }
      // Copy
      if (cmdKey && e.key === 'c') {
        e.preventDefault();
        const selectedLayers = selectedLayerIds
          .map((id) => layers.find((l) => l.id === id))
          .filter(Boolean);
        if (selectedLayers.length > 0) {
          clipboardData = {
            layers: selectedLayers.map((layer) => ({
              type: layer!.type,
              name: layer!.name,
              size: { ...layer!.size },
              data: JSON.parse(JSON.stringify(layer!.data)) as LayerData,
              opacity: layer!.opacity,
              rotation: layer!.rotation,
            })),
          };
        }
        return;
      }
      // Cut
      if (cmdKey && e.key === 'x') {
        e.preventDefault();
        const selectedLayers = selectedLayerIds
          .map((id) => layers.find((l) => l.id === id))
          .filter(Boolean);
        if (selectedLayers.length > 0) {
          clipboardData = {
            layers: selectedLayers.map((layer) => ({
              type: layer!.type,
              name: layer!.name,
              size: { ...layer!.size },
              data: JSON.parse(JSON.stringify(layer!.data)) as LayerData,
              opacity: layer!.opacity,
              rotation: layer!.rotation,
            })),
          };
          selectedLayerIds.forEach((id) => deleteLayer(id));
        }
        return;
      }
      // Paste
      if (cmdKey && e.key === 'v') {
        e.preventDefault();
        if (clipboardData && clipboardData.layers.length > 0) {
          const newIds: string[] = [];
          clipboardData.layers.forEach((layerData, index) => {
            const newId = addLayer({
              type: layerData.type,
              name: `${layerData.name} (copy)`,
              visible: true,
              locked: false,
              opacity: layerData.opacity,
              position: {
                x: canvas.width / 2 - layerData.size.width / 2 + index * 20,
                y: canvas.height / 2 - layerData.size.height / 2 + index * 20,
              },
              size: { ...layerData.size },
              rotation: layerData.rotation,
              data: layerData.data,
            });
            newIds.push(newId);
          });
          // Select the pasted layers
          deselectAll();
          newIds.forEach((id, i) => selectLayer(id, i > 0));
          saveToHistory('Pasted layers');
        }
        return;
      }
      // Duplicate
      if (cmdKey && e.key === 'd') {
        e.preventDefault();
        selectedLayerIds.forEach((id) => duplicateLayer(id));
        return;
      }
      // Select all
      if (cmdKey && e.key === 'a') {
        e.preventDefault();
        deselectAll();
        layers.forEach((layer, i) => selectLayer(layer.id, i > 0));
        return;
      }
      // Escape - deselect all
      if (e.key === 'Escape') {
        e.preventDefault();
        deselectAll();
        return;
      }
      // Arrow keys - nudge
      if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
        if (selectedLayerIds.length === 0) return;
        e.preventDefault();
        const nudgeAmount = e.shiftKey ? 10 : 1;
        const delta = { x: 0, y: 0 };
        switch (e.key) {
          case 'ArrowUp':
            delta.y = -nudgeAmount;
            break;
          case 'ArrowDown':
            delta.y = nudgeAmount;
            break;
          case 'ArrowLeft':
            delta.x = -nudgeAmount;
            break;
          case 'ArrowRight':
            delta.x = nudgeAmount;
            break;
        }
        selectedLayerIds.forEach((id) => {
          const layer = layers.find((l) => l.id === id);
          if (layer && !layer.locked) {
            updateLayer(id, {
              position: {
                x: layer.position.x + delta.x,
                y: layer.position.y + delta.y,
              },
            });
          }
        });
        saveToHistory('Nudged layers');
        return;
      }
      // Zoom shortcuts
      if (cmdKey && (e.key === '=' || e.key === '+')) {
        e.preventDefault();
        setZoom(canvas.zoom + 0.1);
        return;
      }
      if (cmdKey && e.key === '-') {
        e.preventDefault();
        setZoom(canvas.zoom - 0.1);
        return;
      }
      // Zoom to 100%
      if (cmdKey && e.key === '1') {
        e.preventDefault();
        setZoom(1);
        return;
      }
      // Zoom to fit (approximate)
      if (cmdKey && e.key === '0') {
        e.preventDefault();
        const container = document.querySelector('.flex-1.overflow-hidden');
        if (container) {
          const containerWidth = container.clientWidth - 100;
          const containerHeight = container.clientHeight - 100;
          const fitZoom = Math.min(
            containerWidth / canvas.width,
            containerHeight / canvas.height
          );
          setZoom(Math.min(fitZoom, 1));
        }
        return;
      }
    },
    [
      selectedLayerIds,
      layers,
      deleteLayer,
      duplicateLayer,
      updateLayer,
      addLayer,
      selectLayer,
      deselectAll,
      undo,
      redo,
      canvas,
      setZoom,
      saveToHistory,
      checkInputFocus,
    ]
  );
  useEffect(() => {
    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [handleKeyDown]);
}