This file is a merged representation of a subset of the codebase, containing specifically included files, combined into a single document by Repomix.

<file_summary>
This section contains a summary of this file.

<purpose>
This file contains a packed representation of a subset of the repository's contents that is considered the most important context.
It is designed to be easily consumable by AI systems for analysis, code review,
or other automated processes.
</purpose>

<file_format>
The content is organized as follows:
1. This summary section
2. Repository information
3. Directory structure
4. Repository files (if enabled)
5. Multiple file entries, each consisting of:
  - File path as an attribute
  - Full contents of the file
</file_format>

<usage_guidelines>
- This file should be treated as read-only. Any changes should be made to the
  original repository files, not this packed version.
- When processing this file, use the file path to distinguish
  between different files in the repository.
- Be aware that this file may contain sensitive information. Handle it with
  the same level of security as you would the original repository.
</usage_guidelines>

<notes>
- Some files may have been excluded based on .gitignore rules and Repomix's configuration
- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files
- Only files matching these patterns are included: src/app/editor, src/components, src/app/api
- Files matching patterns in .gitignore are excluded
- Files matching default ignore patterns are excluded
- Files are sorted by Git change count (files with more changes are at the bottom)
</notes>

</file_summary>

<directory_structure>
src/
  app/
    api/
      [[...route]]/
        route.ts
    editor/
      page.tsx
  components/
    datatable/
      filters.tsx
      index.tsx
      pagination.tsx
      search-filter.tsx
      types.ts
    rich-editor/
      components/
        bubble-menu/
          link-bubble-menu.tsx
        image/
          image-edit-block.tsx
          image-edit-dialog.tsx
        link/
          link-edit-block.tsx
          link-edit-popover.tsx
          link-popover-block.tsx
        section/
          five.tsx
          four.tsx
          one.tsx
          three.tsx
          two.tsx
        measured-container.tsx
        shortcut-key.tsx
        spinner.tsx
        toolbar-button.tsx
        toolbar-section.tsx
      extensions/
        code-block-lowlight/
          code-block-lowlight.ts
          index.ts
        color/
          color.ts
          index.ts
        file-handler/
          index.ts
        horizontal-rule/
          horizontal-rule.ts
          index.ts
        image/
          index.ts
        link/
          index.ts
          link.ts
        reset-marks-on-enter/
          index.ts
          reset-marks-on-enter.ts
        selection/
          index.ts
          selection.ts
        unset-all-marks/
          index.ts
          unset-all-marks.ts
        index.ts
      hooks/
        use-container-size.ts
        use-minimal-tiptap.ts
        use-theme.ts
        use-throttle.ts
      styles/
        partials/
          code.css
          lists.css
          placeholder.css
          typography.css
          zoom.css
        index.css
      index.ts
      minimal-tiptap.tsx
      types.ts
      utils.ts
    skeletons/
      form-skeletons.tsx
      table-skeleton.tsx
    ui/
      accordion.tsx
      alert-dialog.tsx
      alert.tsx
      aspect-ratio.tsx
      avatar.tsx
      badge.tsx
      breadcrumb.tsx
      button.tsx
      calendar.tsx
      card.tsx
      carousel.tsx
      chart.tsx
      checkbox.tsx
      circular-progress.tsx
      collapsible.tsx
      combobox.tsx
      command.tsx
      context-menu.tsx
      copy-button.tsx
      datetime-picker.tsx
      delete-alert.tsx
      dialog.tsx
      drawer.tsx
      dropdown-menu.tsx
      form.tsx
      hover-card.tsx
      input-otp.tsx
      input.tsx
      label.tsx
      menubar.tsx
      multi-select.tsx
      navigation-menu.tsx
      number-input.tsx
      pagination.tsx
      password-input.tsx
      popover.tsx
      progress.tsx
      radio-group.tsx
      resizable.tsx
      scroll-area.tsx
      select.tsx
      separator.tsx
      sheet.tsx
      sidebar.tsx
      skeleton.tsx
      slider.tsx
      sonner.tsx
      switch.tsx
      table.tsx
      tabs.tsx
      tags-input.tsx
      testimonial-card.tsx
      textarea.tsx
      time-format.tsx
      toggle-group.tsx
      toggle.tsx
      tooltip.tsx
    analytics-tracker.tsx
    CanvasDateElement.tsx
    CanvasEditor.tsx
</directory_structure>

<files>
This section contains the contents of the repository's files.

<file path="src/app/api/[[...route]]/route.ts">
import { LOCAL_MEDIA_PREFIX, LOCAL_UPLOAD_DIR } from '@/config/constants';
import aiRouter from '@/server/ai/ai-routes';
import authRouter from '@/server/auth/auth-routes';
import billingRouter from '@/server/billing/billing-routes';
import commonRouter from '@/server/common/common-routes';
import mediaRouter from '@/server/media/media-routes';
import planRouter from '@/server/plans/plan-routes';
import postRouter from '@/server/posts/post-routes';
import settingRouter from '@/server/settings/setting-routes';
import subscriptionRouter from '@/server/subscriptions/subscription-routes';
import userRouter from '@/server/users/user-routes';
import { Hono } from 'hono';
import { compress } from 'hono/compress';
import { cors } from 'hono/cors';
import { secureHeaders } from 'hono/secure-headers';
import { serveStatic } from 'hono/serve-static';
import { handle } from 'hono/vercel';
import httpStatus from 'http-status';
import path from 'path';

import APIError from '@/lib/api-error';
import { getFileByPath } from '@/lib/file';

// Create Hono app with base path /api
const app = new Hono().basePath('/api');

// Enable compression, CORS, and secure headers
app.use(compress());
app.use(cors());
app.use(secureHeaders());

// static files
app.use(
  `/${LOCAL_MEDIA_PREFIX}/*`,
  serveStatic({
    async getContent(p, c) {
      const fileName = p.split('/').pop();
      if (!fileName) {
        return null;
      }
      const filePath = path.join(process.cwd(), LOCAL_UPLOAD_DIR, fileName);

      const file = await getFileByPath(filePath);

      if (!file) {
        return null;
      }

      c.header('Content-Type', file.mime);
      c.header('Cache-Control', 'public, max-age=31536000');
      c.header('Content-Length', file.size.toString());
      c.header('Content-Disposition', `attachment; filename="${fileName}"`);

      return c.body(file.data);
    },
  }),
);

// Define routes and error handling
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const routes = app
  .route('/auth', authRouter)
  .route('/users', userRouter)
  .route('/settings', settingRouter)
  .route('/media', mediaRouter)
  .route('/posts', postRouter)
  .route('/plans', planRouter)
  .route('/billing', billingRouter)
  .route('/subscriptions', subscriptionRouter)
  .route('/ai', aiRouter)
  .route('/common', commonRouter)
  // Return 404 if route not found
  .notFound((c) => {
    return c.json(
      {
        message: 'Not found',
        code: httpStatus.NOT_FOUND,
      },
      httpStatus.NOT_FOUND,
    );
  })
  // Handle errors
  .onError((err, c) => {
    if (err instanceof APIError) {
      return c.json(
        {
          message: err.message,
          code: err.code,
        },
        err.code,
      );
    }
    console.error(err);

    return c.json(
      {
        message: process.env.NODE_ENV === 'development' ? err.message : 'Internal server error',
        code: httpStatus.INTERNAL_SERVER_ERROR,
      },
      httpStatus.INTERNAL_SERVER_ERROR,
    );
  });

// Export handlers for all HTTP methods
export const GET = handle(app);
export const POST = handle(app);
export const PUT = handle(app);
export const PATCH = handle(app);
export const OPTIONS = handle(app);
export const DELETE = handle(app);

// Type definition for routes
export type ApiTypes = typeof routes;
</file>

<file path="src/app/editor/page.tsx">
// src/app/editor/page.tsx
'use client';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useSearchParams } from 'next/navigation';
import dynamic from 'next/dynamic';
import {
  Download, Type, Image as ImageIcon, Layout, Trash2, Undo2, Redo2,
  PlusSquare, RotateCw, ImageMinus, FlipHorizontal, FlipVertical,
  Layers, Square, Circle as CircleIcon,
  AlignCenter, AlignLeft, AlignRight, Bold, Italic, Underline,
  Sun, Contrast, Droplets, Sparkles, PanelLeft, X, Check, Ruler,
  ShoppingBag, Smartphone, Monitor
} from 'lucide-react';

// Tipos inline
type FilterType = 'none' | 'grayscale' | 'sepia';
type ElementType = 'product' | 'background' | 'text' | 'shape' | 'date' | 'image' | 'sticker' | null;

interface TextElement {
  id: string; text: string; x: number; y: number; fontSize: number; fill: string;
  rotation: number; fontFamily: string; align: 'left' | 'center' | 'right';
  opacity: number; blurRadius: number; shadowEnabled: boolean; shadowColor: string;
  shadowBlur: number; shadowOffsetX: number; shadowOffsetY: number; shadowOpacity: number;
  reflectionEnabled: boolean; flipX: number; flipY: number; filter: FilterType;
  textDecoration?: 'none' | 'underline' | 'line-through' | 'bold';
  fontStyle?: 'normal' | 'italic'; stroke?: string; strokeWidth?: number;
}

interface ShapeElement {
  id: string; type: 'rect' | 'circle' | 'line'; x: number; y: number;
  width?: number; height?: number; radius?: number; fill?: string;
  stroke: string; strokeWidth: number; rotation: number; points?: number[];
  opacity: number; blurRadius: number; shadowEnabled: boolean; shadowColor: string;
  shadowBlur: number; shadowOffsetX: number; shadowOffsetY: number; shadowOpacity: number;
  reflectionEnabled: boolean; flipX: number; flipY: number; filter: FilterType;
}

interface DateElement {
  id: string; text: string; format: string; x: number; y: number; fontSize: number;
  fill: string; rotation: number; fontFamily: string; opacity: number; blurRadius: number;
  shadowEnabled: boolean; shadowColor: string; shadowBlur: number; shadowOffsetX: number;
  shadowOffsetY: number; shadowOpacity: number; reflectionEnabled: boolean;
  flipX: number; flipY: number; filter: FilterType;
}

interface ImageElement {
  id: string; url: string; x: number; y: number; scaleX: number; scaleY: number;
  rotation: number; opacity: number; blurRadius: number; shadowEnabled: boolean;
  shadowColor: string; shadowBlur: number; shadowOffsetX: number; shadowOffsetY: number;
  shadowOpacity: number; reflectionEnabled: boolean; flipX: number; flipY: number;
  filter: FilterType; width?: number; height?: number;
}

interface StickerElement {
  id: string;
  type: 'PROMO' | 'OFERTA' | 'DESCUENTO' | 'NEW' | 'SALE' | 'VIP';
  x: number; y: number;
  rotation: number; opacity: number; scaleX: number; scaleY: number;
}

interface CanvasState {
  productX: number; productY: number; productScale: number; productRotation: number;
  productOpacity: number; productBlurRadius: number;
  productShadowEnabled: boolean; productShadowColor: string; productShadowBlur: number;
  productShadowOffsetX: number; productShadowOffsetY: number; productShadowOpacity: number;
  productReflectionEnabled: boolean; productFlipX: number; productFlipY: number; productFilter: FilterType;
  backgroundOpacity: number; backgroundBlurRadius: number;
  backgroundShadowEnabled: boolean; backgroundShadowColor: string; backgroundShadowBlur: number;
  backgroundShadowOffsetX: number; backgroundShadowOffsetY: number; backgroundShadowOpacity: number;
  backgroundReflectionEnabled: boolean; backgroundFlipX: number; backgroundFlipY: number; backgroundFilter: FilterType;
  selectedPresetBackgroundUrl?: string;
  textElements: TextElement[]; shapeElements: ShapeElement[];
  dateElement: DateElement | null; imageElements: ImageElement[]; stickerElements: StickerElement[];
  canvasBackgroundColor: string;
}

interface DisplayBackground { id: string; url: string; isPremium: boolean; }

// Constantes
const DEFAULT_CANVAS_SIZE = 600;
const MAX_HISTORY = 40;
const BASE_EL = {
  rotation: 0, opacity: 1, blurRadius: 0,
  shadowEnabled: false, shadowColor: '#000000', shadowBlur: 10,
  shadowOffsetX: 4, shadowOffsetY: 4, shadowOpacity: 0.4,
  reflectionEnabled: false, flipX: 1, flipY: 1, filter: 'none' as FilterType,
};

// Presets de tamaño
const SIZE_PRESETS = [
  { label: 'Instagram (1:1)', width: 1080, height: 1080 },
  { label: 'Instagram Story', width: 1080, height: 1920 },
  { label: 'Facebook Post', width: 940, height: 788 },
  { label: 'Facebook Cover', width: 820, height: 312 },
  { label: 'Producto (600x600)', width: 600, height: 600 },
];

// Dynamic import
const DynamicCanvasEditor = dynamic(() => import('@/components/CanvasEditor'), { ssr: false });

// Helpers
function formatDate(date: Date, fmt: string): string {
  const opts: Intl.DateTimeFormatOptions = {};
  if (fmt === 'DD/MM/YYYY') { opts.day = '2-digit'; opts.month = '2-digit'; opts.year = 'numeric'; }
  else if (fmt === 'MMMM DD, YYYY') { opts.month = 'long'; opts.day = 'numeric'; opts.year = 'numeric'; }
  else if (fmt === 'YYYY-MM-DD') { opts.year = 'numeric'; opts.month = '2-digit'; opts.day = '2-digit'; }
  else { opts.day = '2-digit'; opts.month = '2-digit'; opts.year = 'numeric'; }
  return new Intl.DateTimeFormat('es-ES', opts).format(date);
}

// Componentes UI
const Slider: React.FC<{
  label: string; value: number; min: number; max: number; step?: number;
  display?: string; onChange: (v: number) => void; onCommit?: () => void;
}> = ({ label, value, min, max, step = 1, display, onChange, onCommit }) => (
  <div className="mb-3">
    <div className="flex justify-between mb-1">
      <label className="text-xs text-gray-500">{label}</label>
      <span className="text-xs text-gray-700 font-medium">{display ?? value}</span>
    </div>
    <input
      type="range" min={min} max={max} step={step} value={value}
      onChange={(e) => onChange(Number(e.target.value))}
      onMouseUp={onCommit} onTouchEnd={onCommit}
      className="w-full h-1.5 rounded-full appearance-none cursor-pointer accent-blue-500 bg-gray-200"
    />
  </div>
);

const Toggle: React.FC<{ label: string; value: boolean; onChange: (v: boolean) => void }> = ({ label, value, onChange }) => (
  <div className="flex items-center justify-between py-2">
    <span className="text-sm text-gray-700">{label}</span>
    <button
      onClick={() => onChange(!value)}
      className={`relative w-9 h-5 rounded-full transition-colors ${value ? 'bg-blue-500' : 'bg-gray-300'}`}
    >
      <div className={`absolute top-0.5 w-4 h-4 bg-white rounded-full transition-transform ${value ? 'left-4' : 'left-0.5'}`} />
    </button>
  </div>
);

const SecHead: React.FC<{ title: string; icon?: React.ReactNode }> = ({ title, icon }) => (
  <div className="flex items-center gap-2 mb-3 pb-2 border-b border-gray-100">
    {icon && <span className="text-gray-400">{icon}</span>}
    <h3 className="text-sm font-semibold text-gray-700">{title}</h3>
  </div>
);

// Página Principal
export default function EditorPage() {
  const searchParams = useSearchParams();
  const initialUrl = searchParams.get('url') || '';

  // Estado Canvas (Dimensiones)
  const [canvasWidth, setCanvasWidth] = useState(DEFAULT_CANVAS_SIZE);
  const [canvasHeight, setCanvasHeight] = useState(DEFAULT_CANVAS_SIZE);

  // Estado General
  const [konvaCanvas, setKonvaCanvas] = useState<HTMLCanvasElement | null>(null);
  const [canvasBackgroundColor, setCanvasBackgroundColor] = useState('#ffffff');
  const [selectedCanvasElement, setSelectedCanvasElement] = useState<ElementType>('product');
  const [selectedElementId, setSelectedElementId] = useState<string | null>(null);
  const [showCenterGuides, setShowCenterGuides] = useState(true);

  // Producto
  const [productImageUrl, setProductImageUrl] = useState(initialUrl);
  const [productX, setProductX] = useState(DEFAULT_CANVAS_SIZE / 2);
  const [productY, setProductY] = useState(DEFAULT_CANVAS_SIZE / 2);
  const [productScale, setProductScale] = useState(1);
  const [productRotation, setProductRotation] = useState(0);
  const [productOpacity, setProductOpacity] = useState(1);
  const [productBlurRadius, setProductBlurRadius] = useState(0);
  const [productShadowEnabled, setProductShadowEnabled] = useState(false);
  const [productShadowColor, setProductShadowColor] = useState('#000000');
  const [productShadowBlur, setProductShadowBlur] = useState(10);
  const [productShadowOffsetX, setProductShadowOffsetX] = useState(4);
  const [productShadowOffsetY, setProductShadowOffsetY] = useState(4);
  const [productShadowOpacity, setProductShadowOpacity] = useState(0.4);
  const [productReflectionEnabled, setProductReflectionEnabled] = useState(false);
  const [productFlipX, setProductFlipX] = useState(1);
  const [productFlipY, setProductFlipY] = useState(1);
  const [productFilter, setProductFilter] = useState<FilterType>('none');
  const [hasProductBeenScaledManually, setHasProductBeenScaledManually] = useState(false);

  // Fondo
  const [selectedPresetBackgroundUrl, setSelectedPresetBackgroundUrl] = useState<string | undefined>(undefined);
  const [backgroundOpacity, setBackgroundOpacity] = useState(1);
  const [backgroundBlurRadius, setBackgroundBlurRadius] = useState(0);
  const [backgroundShadowEnabled, setBackgroundShadowEnabled] = useState(false);
  const [backgroundShadowColor, setBackgroundShadowColor] = useState('#000000');
  const [backgroundShadowBlur, setBackgroundShadowBlur] = useState(10);
  const [backgroundShadowOffsetX, setBackgroundShadowOffsetX] = useState(0);
  const [backgroundShadowOffsetY, setBackgroundShadowOffsetY] = useState(0);
  const [backgroundShadowOpacity, setBackgroundShadowOpacity] = useState(0.4);
  const [backgroundReflectionEnabled, setBackgroundReflectionEnabled] = useState(false);
  const [backgroundFlipX, setBackgroundFlipX] = useState(1);
  const [backgroundFlipY, setBackgroundFlipY] = useState(1);
  const [backgroundFilter, setBackgroundFilter] = useState<FilterType>('none');

  // Elementos
  const [textElements, setTextElements] = useState<TextElement[]>([]);
  const [shapeElements, setShapeElements] = useState<ShapeElement[]>([]);
  const [dateElement, setDateElement] = useState<DateElement | null>(null);
  const [imageElements, setImageElements] = useState<ImageElement[]>([]);
  const [stickerElements, setStickerElements] = useState<StickerElement[]>([]);

  // Historia
  const [history, setHistory] = useState<CanvasState[]>([]);
  const [historyPointer, setHistoryPointer] = useState(-1);

  // UI
  const [leftPanel, setLeftPanel] = useState<'backgrounds' | 'stickers' | 'sizes'>('backgrounds');
  const [mobilePanel, setMobilePanel] = useState<'none' | 'left' | 'right'>('none');
  const [isUserPremium] = useState(false);
  const [adminBackgrounds, setAdminBackgrounds] = useState<DisplayBackground[]>([]);
  const [zOrderAction, setZOrderAction] = useState<{
    type: 'up' | 'down' | 'top' | 'bottom';
    id: string;
    elementType: Exclude<ElementType, null>;
  } | null>(null);
  const [imageToMoveToBottomId, setImageToMoveToBottomId] = useState<string | null>(null);

  const productFileRef = useRef<HTMLInputElement>(null);
  const elementImageRef = useRef<HTMLInputElement>(null);
  const bgFileRef = useRef<HTMLInputElement>(null);

  // Fondos predefinidos
  const hardcodedBgs: DisplayBackground[] = [
    { id: 'bg-1', url: 'https://images.unsplash.com/photo-1557682250-33bd709cbe85?w=700&q=80', isPremium: false },
    { id: 'bg-2', url: 'https://images.unsplash.com/photo-1557682224-5b8590cd9ec5?w=700&q=80', isPremium: false },
    { id: 'bg-3', url: 'https://images.unsplash.com/photo-1557682260-96773eb01377?w=700&q=80', isPremium: false },
    { id: 'bg-4', url: 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=700&q=80', isPremium: false },
    { id: 'bg-5', url: 'https://images.unsplash.com/photo-1579546929518-9e396f3cc809?w=700&q=80', isPremium: false },
    { id: 'bg-6', url: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=700&q=80', isPremium: false },
    { id: 'bg-7', url: 'https://images.unsplash.com/photo-1618005198919-d3d4b5a92ead?w=700&q=80', isPremium: true },
    { id: 'bg-8', url: 'https://images.unsplash.com/photo-1565214975484-3cfa9e56f914?w=700&q=80', isPremium: true },
    { id: 'c-1', url: '#1a1a2e', isPremium: false },
    { id: 'c-2', url: '#f8f9fa', isPremium: false },
    { id: 'c-3', url: '#e8f4f8', isPremium: false },
    { id: 'c-4', url: '#fef9e7', isPremium: false },
    { id: 'c-5', url: '#f3e5f5', isPremium: false },
    { id: 'c-6', url: '#e8f5e9', isPremium: false },
  ];

  const allBgs = [...hardcodedBgs, ...adminBackgrounds];

  // Cargar fondos del API
  useEffect(() => {
    fetch('/api/media?type=backgrounds&isUserPremium=' + isUserPremium)
      .then(r => r.ok ? r.json() : null)
      .then(data => {
        if (data?.docs) {
          setAdminBackgrounds(data.docs.map((m: any) => ({ id: m.id, url: m.url, isPremium: m.isPremium })));
        }
      })
      .catch(() => { });
  }, [isUserPremium]);

  // Auto-escala del producto
  useEffect(() => {
    if (!productImageUrl || hasProductBeenScaledManually) return;
    const img = new window.Image();
    img.onload = () => {
      const maxDim = Math.max(img.width, img.height);
      const target = Math.min(canvasWidth, canvasHeight) * 0.75;
      if (maxDim > target) setProductScale(target / maxDim);
      setProductX(canvasWidth / 2);
      setProductY(canvasHeight / 2);
    };
    img.src = productImageUrl;
  }, [productImageUrl, hasProductBeenScaledManually, canvasWidth, canvasHeight]);

  // Historia
  const getCurrentState = useCallback((): CanvasState => ({
    productX, productY, productScale, productRotation, productOpacity, productBlurRadius,
    productShadowEnabled, productShadowColor, productShadowBlur, productShadowOffsetX,
    productShadowOffsetY, productShadowOpacity, productReflectionEnabled,
    productFlipX, productFlipY, productFilter,
    backgroundOpacity, backgroundBlurRadius, backgroundShadowEnabled, backgroundShadowColor,
    backgroundShadowBlur, backgroundShadowOffsetX, backgroundShadowOffsetY, backgroundShadowOpacity,
    backgroundReflectionEnabled, backgroundFlipX, backgroundFlipY, backgroundFilter,
    selectedPresetBackgroundUrl,
    textElements: JSON.parse(JSON.stringify(textElements)),
    shapeElements: JSON.parse(JSON.stringify(shapeElements)),
    dateElement: dateElement ? JSON.parse(JSON.stringify(dateElement)) : null,
    imageElements: JSON.parse(JSON.stringify(imageElements)),
    stickerElements: JSON.parse(JSON.stringify(stickerElements)),
    canvasBackgroundColor,
  }), [
    productX, productY, productScale, productRotation, productOpacity, productBlurRadius,
    productShadowEnabled, productShadowColor, productShadowBlur, productShadowOffsetX,
    productShadowOffsetY, productShadowOpacity, productReflectionEnabled,
    productFlipX, productFlipY, productFilter,
    backgroundOpacity, backgroundBlurRadius, backgroundShadowEnabled, backgroundShadowColor,
    backgroundShadowBlur, backgroundShadowOffsetX, backgroundShadowOffsetY, backgroundShadowOpacity,
    backgroundReflectionEnabled, backgroundFlipX, backgroundFlipY, backgroundFilter,
    selectedPresetBackgroundUrl, textElements, shapeElements, dateElement, imageElements, stickerElements, canvasBackgroundColor,
  ]);

  const saveState = useCallback(() => {
    const state = getCurrentState();
    setHistory(prev => {
      const sliced = prev.slice(0, historyPointer + 1);
      if (sliced.length > 0 && JSON.stringify(sliced[sliced.length - 1]) === JSON.stringify(state)) return prev;
      const next = [...sliced, state].slice(-MAX_HISTORY);
      setHistoryPointer(next.length - 1);
      return next;
    });
  }, [getCurrentState, historyPointer]);

  const onTransformEndCommit = useCallback(() => {
    if (selectedCanvasElement === 'product') setHasProductBeenScaledManually(true);
    saveState();
  }, [selectedCanvasElement, saveState]);

  const applyState = useCallback((state: CanvasState) => {
    setProductX(state.productX); setProductY(state.productY);
    setProductScale(state.productScale); setProductRotation(state.productRotation);
    setProductOpacity(state.productOpacity); setProductBlurRadius(state.productBlurRadius);
    setProductShadowEnabled(state.productShadowEnabled); setProductShadowColor(state.productShadowColor);
    setProductShadowBlur(state.productShadowBlur); setProductShadowOffsetX(state.productShadowOffsetX);
    setProductShadowOffsetY(state.productShadowOffsetY); setProductShadowOpacity(state.productShadowOpacity);
    setProductReflectionEnabled(state.productReflectionEnabled);
    setProductFlipX(state.productFlipX); setProductFlipY(state.productFlipY); setProductFilter(state.productFilter);
    setBackgroundOpacity(state.backgroundOpacity); setBackgroundBlurRadius(state.backgroundBlurRadius);
    setBackgroundShadowEnabled(state.backgroundShadowEnabled); setBackgroundShadowColor(state.backgroundShadowColor);
    setBackgroundShadowBlur(state.backgroundShadowBlur); setBackgroundShadowOffsetX(state.backgroundShadowOffsetX);
    setBackgroundShadowOffsetY(state.backgroundShadowOffsetY); setBackgroundShadowOpacity(state.backgroundShadowOpacity);
    setBackgroundReflectionEnabled(state.backgroundReflectionEnabled);
    setBackgroundFlipX(state.backgroundFlipX); setBackgroundFlipY(state.backgroundFlipY); setBackgroundFilter(state.backgroundFilter);
    setSelectedPresetBackgroundUrl(state.selectedPresetBackgroundUrl);
    setTextElements(state.textElements); setShapeElements(state.shapeElements);
    setDateElement(state.dateElement); setImageElements(state.imageElements);
    setStickerElements(state.stickerElements);
    setCanvasBackgroundColor(state.canvasBackgroundColor);
  }, []);

  const handleUndo = useCallback(() => {
    if (historyPointer > 0) { const p = historyPointer - 1; setHistoryPointer(p); applyState(history[p]); }
  }, [history, historyPointer, applyState]);

  const handleRedo = useCallback(() => {
    if (historyPointer < history.length - 1) { const p = historyPointer + 1; setHistoryPointer(p); applyState(history[p]); }
  }, [history, historyPointer, applyState]);

  // Selección
  const handleElementSelect = useCallback((type: ElementType, id?: string) => {
    setSelectedCanvasElement(type);
    setSelectedElementId(id ?? null);
    if (id) setMobilePanel('right');
  }, []);

  // Adds
  const handleAddText = useCallback(() => {
    const el: TextElement = {
      id: `text-${Date.now()}`, text: 'Tu texto aquí',
      x: canvasWidth / 2, y: canvasHeight / 2,
      fontSize: 32, fill: '#111111', fontFamily: 'Arial', align: 'center',
      ...BASE_EL,
    };
    setTextElements(p => [...p, el]);
    handleElementSelect('text', el.id);
    onTransformEndCommit();
  }, [handleElementSelect, onTransformEndCommit, canvasWidth, canvasHeight]);

  const handleAddRect = useCallback(() => {
    const el: ShapeElement = {
      id: `shape-${Date.now()}`, type: 'rect',
      x: canvasWidth / 2, y: canvasHeight / 2,
      width: 120, height: 80,
      fill: '#3b82f6', stroke: 'transparent', strokeWidth: 0,
      ...BASE_EL,
    };
    setShapeElements(p => [...p, el]);
    handleElementSelect('shape', el.id);
    onTransformEndCommit();
  }, [handleElementSelect, onTransformEndCommit, canvasWidth, canvasHeight]);

  const handleAddCircle = useCallback(() => {
    const el: ShapeElement = {
      id: `shape-${Date.now()}`, type: 'circle',
      x: canvasWidth / 2, y: canvasHeight / 2,
      radius: 60, fill: '#f59e0b', stroke: 'transparent', strokeWidth: 0,
      ...BASE_EL,
    };
    setShapeElements(p => [...p, el]);
    handleElementSelect('shape', el.id);
    onTransformEndCommit();
  }, [handleElementSelect, onTransformEndCommit, canvasWidth, canvasHeight]);

  const handleAddDate = useCallback(() => {
    const fmt = 'DD/MM/YYYY';
    const el: DateElement = {
      id: `date-${Date.now()}`, text: formatDate(new Date(), fmt), format: fmt,
      x: canvasWidth / 2, y: canvasHeight / 2 + 80,
      fontSize: 22, fill: '#333333', fontFamily: 'Arial',
      ...BASE_EL,
    };
    setDateElement(el);
    handleElementSelect('date', el.id);
    onTransformEndCommit();
  }, [handleElementSelect, onTransformEndCommit, canvasWidth, canvasHeight]);

  const handleAddSticker = useCallback((type: 'PROMO' | 'OFERTA' | 'DESCUENTO' | 'NEW' | 'SALE' | 'VIP') => {
    const el: StickerElement = {
      id: `sticker-${Date.now()}`,
      type,
      x: canvasWidth / 2,
      y: canvasHeight / 2,
      rotation: 0, opacity: 1,
      scaleX: 1, scaleY: 1,
    };
    setStickerElements(p => [...p, el]);
    handleElementSelect('sticker', el.id);
    onTransformEndCommit();
  }, [handleElementSelect, onTransformEndCommit, canvasWidth, canvasHeight]);

  // Delete
  const handleDeleteElement = useCallback(() => {
    if (!selectedCanvasElement) return;
    if (selectedCanvasElement === 'text' && selectedElementId)
      setTextElements(p => p.filter(e => e.id !== selectedElementId));
    else if (selectedCanvasElement === 'shape' && selectedElementId)
      setShapeElements(p => p.filter(e => e.id !== selectedElementId));
    else if (selectedCanvasElement === 'date') setDateElement(null);
    else if (selectedCanvasElement === 'image' && selectedElementId)
      setImageElements(p => p.filter(e => e.id !== selectedElementId));
    else if (selectedCanvasElement === 'sticker' && selectedElementId)
      setStickerElements(p => p.filter(e => e.id !== selectedElementId));
    else if (selectedCanvasElement === 'background') {
      setSelectedPresetBackgroundUrl(undefined);
      setCanvasBackgroundColor('#ffffff');
    }
    setSelectedCanvasElement(null);
    setSelectedElementId(null);
    onTransformEndCommit();
  }, [selectedCanvasElement, selectedElementId, onTransformEndCommit]);

  // Duplicate
  const handleDuplicate = useCallback(() => {
    if (selectedCanvasElement === 'text' && selectedElementId) {
      const orig = textElements.find(e => e.id === selectedElementId);
      if (orig) {
        const el = { ...orig, id: `text-${Date.now()}`, x: orig.x + 20, y: orig.y + 20 };
        setTextElements(p => [...p, el]); handleElementSelect('text', el.id);
      }
    } else if (selectedCanvasElement === 'shape' && selectedElementId) {
      const orig = shapeElements.find(e => e.id === selectedElementId);
      if (orig) {
        const el = { ...orig, id: `shape-${Date.now()}`, x: orig.x + 20, y: orig.y + 20 };
        setShapeElements(p => [...p, el]); handleElementSelect('shape', el.id);
      }
    } else if (selectedCanvasElement === 'image' && selectedElementId) {
      const orig = imageElements.find(e => e.id === selectedElementId);
      if (orig) {
        const el = { ...orig, id: `image-${Date.now()}`, x: orig.x + 20, y: orig.y + 20 };
        setImageElements(p => [...p, el]); handleElementSelect('image', el.id);
      }
    } else if (selectedCanvasElement === 'sticker' && selectedElementId) {
      const orig = stickerElements.find(e => e.id === selectedElementId);
      if (orig) {
        const el = { ...orig, id: `sticker-${Date.now()}`, x: orig.x + 20, y: orig.y + 20 };
        setStickerElements(p => [...p, el]); handleElementSelect('sticker', el.id);
      }
    }
    onTransformEndCommit();
  }, [selectedCanvasElement, selectedElementId, textElements, shapeElements, imageElements, stickerElements, handleElementSelect, onTransformEndCommit]);

  // Updates
  const updateText = useCallback((id: string, u: Partial<TextElement>) => {
    setTextElements(p => p.map(e => e.id === id ? { ...e, ...u } : e));
  }, []);

  const updateShape = useCallback((id: string, u: Partial<ShapeElement>) => {
    setShapeElements(p => p.map(e => e.id === id ? { ...e, ...u } : e));
  }, []);

  const updateDate = useCallback((u: Partial<DateElement>) => {
    setDateElement(p => p ? { ...p, ...u } : null);
  }, []);

  const updateImage = useCallback((id: string, u: Partial<ImageElement>) => {
    setImageElements(p => p.map(e => e.id === id ? { ...e, ...u } : e));
  }, []);

  const updateSticker = useCallback((id: string, u: Partial<StickerElement>) => {
    setStickerElements(p => p.map(e => e.id === id ? { ...e, ...u } : e));
  }, []);

  const handleImageAddedAndLoaded = useCallback((
    id: string, scaleX: number, scaleY: number, x: number, y: number, w: number, h: number,
  ) => {
    setImageElements(p => p.map(e =>
      e.id === id ? { ...e, x, y, scaleX, scaleY, width: w, height: h } : e
    ));
    onTransformEndCommit();
  }, [onTransformEndCommit]);

  // Fondos
  const handleSelectBg = useCallback((bg: DisplayBackground) => {
    if (bg.isPremium && !isUserPremium) return;
    if (bg.url.startsWith('#')) {
      setCanvasBackgroundColor(bg.url);
      setSelectedPresetBackgroundUrl(undefined);
    } else {
      setSelectedPresetBackgroundUrl(bg.url);
      setCanvasBackgroundColor('#ffffff');
    }
    setBackgroundOpacity(1); setBackgroundBlurRadius(0);
    setBackgroundShadowEnabled(false); setBackgroundReflectionEnabled(false);
    setBackgroundFlipX(1); setBackgroundFlipY(1); setBackgroundFilter('none');
    setSelectedCanvasElement('background');
    setSelectedElementId(null);
    onTransformEndCommit();
    setMobilePanel('none');
  }, [isUserPremium, onTransformEndCommit]);

  // Uploads
  const handleProductUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (!e.target.files?.[0]) return;
    const url = URL.createObjectURL(e.target.files[0]);
    setProductImageUrl(url);
    setProductX(canvasWidth / 2); setProductY(canvasHeight / 2);
    setProductScale(1); setProductRotation(0); setProductOpacity(1);
    setProductBlurRadius(0); setProductShadowEnabled(false);
    setProductFlipX(1); setProductFlipY(1); setProductFilter('none');
    setHasProductBeenScaledManually(false);
    setSelectedCanvasElement('product'); setSelectedElementId(null);
    e.target.value = '';
  };

  const handleElementImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (!e.target.files?.[0]) return;
    const el: ImageElement = {
      id: `image-${Date.now()}`, url: URL.createObjectURL(e.target.files[0]),
      x: canvasWidth / 2, y: canvasHeight / 2, scaleX: 1, scaleY: 1, ...BASE_EL,
    };
    setImageElements(p => [...p, el]);
    handleElementSelect('image', el.id);
    e.target.value = '';
  };

  const handleBgFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (!e.target.files?.[0]) return;
    const url = URL.createObjectURL(e.target.files[0]);
    setSelectedPresetBackgroundUrl(url);
    setCanvasBackgroundColor('#ffffff');
    setSelectedCanvasElement('background'); setSelectedElementId(null);
    onTransformEndCommit();
    e.target.value = '';
    setMobilePanel('none');
  };

  // Flip
  const handleFlip = useCallback((axis: 'x' | 'y') => {
    const toggle = (v: number) => v * -1;
    const selText = textElements.find(e => e.id === selectedElementId);
    const selShape = shapeElements.find(e => e.id === selectedElementId);
    const selImage = imageElements.find(e => e.id === selectedElementId);
    const selSticker = stickerElements.find(e => e.id === selectedElementId);

    if (selectedCanvasElement === 'product') {
      if (axis === 'x') setProductFlipX(toggle); else setProductFlipY(toggle);
    } else if (selectedCanvasElement === 'background') {
      if (axis === 'x') setBackgroundFlipX(toggle); else setBackgroundFlipY(toggle);
    } else if (selectedCanvasElement === 'text' && selText) {
      updateText(selText.id, { flipX: axis === 'x' ? selText.flipX * -1 : selText.flipX, flipY: axis === 'y' ? selText.flipY * -1 : selText.flipY });
    } else if (selectedCanvasElement === 'shape' && selShape) {
      updateShape(selShape.id, { flipX: axis === 'x' ? selShape.flipX * -1 : selShape.flipX, flipY: axis === 'y' ? selShape.flipY * -1 : selShape.flipY });
    } else if (selectedCanvasElement === 'image' && selImage) {
      updateImage(selImage.id, { flipX: axis === 'x' ? selImage.flipX * -1 : selImage.flipX, flipY: axis === 'y' ? selImage.flipY * -1 : selImage.flipY });
    } else if (selectedCanvasElement === 'sticker' && selSticker) {
      updateSticker(selSticker.id, { scaleX: axis === 'x' ? selSticker.scaleX * -1 : selSticker.scaleX, scaleY: axis === 'y' ? selSticker.scaleY * -1 : selSticker.scaleY });
    } else if (selectedCanvasElement === 'date' && dateElement) {
      updateDate({ flipX: axis === 'x' ? dateElement.flipX * -1 : dateElement.flipX, flipY: axis === 'y' ? dateElement.flipY * -1 : dateElement.flipY });
    }
    onTransformEndCommit();
  }, [selectedCanvasElement, selectedElementId, textElements, shapeElements, imageElements, stickerElements, dateElement,
    updateText, updateShape, updateImage, updateSticker, updateDate, onTransformEndCommit]);

  // Filtro
  const handleFilter = useCallback((f: FilterType) => {
    if (selectedCanvasElement === 'product') setProductFilter(f);
    else if (selectedCanvasElement === 'background') setBackgroundFilter(f);
    else if (selectedCanvasElement === 'text' && selectedElementId) updateText(selectedElementId, { filter: f });
    else if (selectedCanvasElement === 'shape' && selectedElementId) updateShape(selectedElementId, { filter: f });
    else if (selectedCanvasElement === 'image' && selectedElementId) updateImage(selectedElementId, { filter: f });
    else if (selectedCanvasElement === 'sticker' && selectedElementId) updateSticker(selectedElementId, { filter: f } as any); // Stickers no soportan filtros en este demo pero evitamos crash
    else if (selectedCanvasElement === 'date' && dateElement) updateDate({ filter: f });
    onTransformEndCommit();
  }, [selectedCanvasElement, selectedElementId, dateElement, updateText, updateShape, updateImage, updateSticker, updateDate, onTransformEndCommit]);

  // Z-order
  const handleZOrder = useCallback((type: 'up' | 'down' | 'top' | 'bottom') => {
    if (!selectedCanvasElement || selectedCanvasElement === 'background') return;
    const id = selectedCanvasElement === 'product' ? 'product' : selectedElementId;
    if (!id) return;
    setZOrderAction({ type, id, elementType: selectedCanvasElement as Exclude<ElementType, null> });
  }, [selectedCanvasElement, selectedElementId]);

  const onZOrderActionComplete = useCallback(() => {
    setZOrderAction(null); saveState();
  }, [saveState]);

  const onImageMovedToBottom = useCallback((id: string) => {
    if (id === imageToMoveToBottomId) setImageToMoveToBottomId(null);
  }, [imageToMoveToBottomId]);

  // Teclado
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      const tag = (document.activeElement as HTMLElement)?.tagName;
      if (tag === 'INPUT' || tag === 'TEXTAREA') return;
      if (e.key === 'Delete' || e.key === 'Backspace') handleDeleteElement();
      if ((e.ctrlKey || e.metaKey) && !e.shiftKey && e.key === 'z') { e.preventDefault(); handleUndo(); }
      if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.shiftKey && e.key === 'z'))) { e.preventDefault(); handleRedo(); }
      if ((e.ctrlKey || e.metaKey) && e.key === 'd') { e.preventDefault(); handleDuplicate(); }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [handleDeleteElement, handleUndo, handleRedo, handleDuplicate]);

  // Download
  const handleDownload = () => {
    if (!konvaCanvas) { alert('Editor no listo, intenta de nuevo.'); return; }
    try {
      const link = document.createElement('a');
      link.href = konvaCanvas.toDataURL('image/png', 1.0);
      link.download = 'editta-design.png';
      link.click();
    } catch { alert('Error al descargar.'); }
  };

  // Props del elemento activo
  const selText = textElements.find(e => e.id === selectedElementId);
  const selShape = shapeElements.find(e => e.id === selectedElementId);
  const selImage = imageElements.find(e => e.id === selectedElementId);
  const selSticker = stickerElements.find(e => e.id === selectedElementId);

  type EP = {
    opacity: number; blurRadius: number;
    shadowEnabled: boolean; shadowColor: string; shadowBlur: number;
    shadowOffsetX: number; shadowOffsetY: number; shadowOpacity: number;
    reflectionEnabled: boolean; filter: FilterType;
    setOpacity: (v: number) => void; setBlurRadius: (v: number) => void;
    setShadowEnabled: (v: boolean) => void; setShadowColor: (v: string) => void;
    setShadowBlur: (v: number) => void; setShadowOffsetX: (v: number) => void;
    setShadowOffsetY: (v: number) => void; setShadowOpacity: (v: number) => void;
    setReflectionEnabled: (v: boolean) => void;
  };

  let ep: EP | null = null;
  if (selectedCanvasElement === 'product') {
    ep = {
      opacity: productOpacity, blurRadius: productBlurRadius,
      shadowEnabled: productShadowEnabled, shadowColor: productShadowColor,
      shadowBlur: productShadowBlur, shadowOffsetX: productShadowOffsetX,
      shadowOffsetY: productShadowOffsetY, shadowOpacity: productShadowOpacity,
      reflectionEnabled: productReflectionEnabled, filter: productFilter,
      setOpacity: setProductOpacity, setBlurRadius: setProductBlurRadius,
      setShadowEnabled: setProductShadowEnabled, setShadowColor: setProductShadowColor,
      setShadowBlur: setProductShadowBlur, setShadowOffsetX: setProductShadowOffsetX,
      setShadowOffsetY: setProductShadowOffsetY, setShadowOpacity: setProductShadowOpacity,
      setReflectionEnabled: setProductReflectionEnabled,
    };
  } else if (selectedCanvasElement === 'background') {
    ep = {
      opacity: backgroundOpacity, blurRadius: backgroundBlurRadius,
      shadowEnabled: backgroundShadowEnabled, shadowColor: backgroundShadowColor,
      shadowBlur: backgroundShadowBlur, shadowOffsetX: backgroundShadowOffsetX,
      shadowOffsetY: backgroundShadowOffsetY, shadowOpacity: backgroundShadowOpacity,
      reflectionEnabled: backgroundReflectionEnabled, filter: backgroundFilter,
      setOpacity: setBackgroundOpacity, setBlurRadius: setBackgroundBlurRadius,
      setShadowEnabled: setBackgroundShadowEnabled, setShadowColor: setBackgroundShadowColor,
      setShadowBlur: setBackgroundShadowBlur, setShadowOffsetX: setBackgroundShadowOffsetX,
      setShadowOffsetY: setBackgroundShadowOffsetY, setShadowOpacity: setBackgroundShadowOpacity,
      setReflectionEnabled: setBackgroundReflectionEnabled,
    };
  } else if (selectedCanvasElement === 'text' && selText) {
    ep = {
      opacity: selText.opacity, blurRadius: selText.blurRadius,
      shadowEnabled: selText.shadowEnabled, shadowColor: selText.shadowColor,
      shadowBlur: selText.shadowBlur, shadowOffsetX: selText.shadowOffsetX,
      shadowOffsetY: selText.shadowOffsetY, shadowOpacity: selText.shadowOpacity,
      reflectionEnabled: selText.reflectionEnabled, filter: selText.filter,
      setOpacity: v => updateText(selText.id, { opacity: v }),
      setBlurRadius: v => updateText(selText.id, { blurRadius: v }),
      setShadowEnabled: v => updateText(selText.id, { shadowEnabled: v }),
      setShadowColor: v => updateText(selText.id, { shadowColor: v }),
      setShadowBlur: v => updateText(selText.id, { shadowBlur: v }),
      setShadowOffsetX: v => updateText(selText.id, { shadowOffsetX: v }),
      setShadowOffsetY: v => updateText(selText.id, { shadowOffsetY: v }),
      setShadowOpacity: v => updateText(selText.id, { shadowOpacity: v }),
      setReflectionEnabled: v => updateText(selText.id, { reflectionEnabled: v }),
    };
  } else if (selectedCanvasElement === 'shape' && selShape) {
    ep = {
      opacity: selShape.opacity, blurRadius: selShape.blurRadius,
      shadowEnabled: selShape.shadowEnabled, shadowColor: selShape.shadowColor,
      shadowBlur: selShape.shadowBlur, shadowOffsetX: selShape.shadowOffsetX,
      shadowOffsetY: selShape.shadowOffsetY, shadowOpacity: selShape.shadowOpacity,
      reflectionEnabled: selShape.reflectionEnabled, filter: selShape.filter,
      setOpacity: v => updateShape(selShape.id, { opacity: v }),
      setBlurRadius: v => updateShape(selShape.id, { blurRadius: v }),
      setShadowEnabled: v => updateShape(selShape.id, { shadowEnabled: v }),
      setShadowColor: v => updateShape(selShape.id, { shadowColor: v }),
      setShadowBlur: v => updateShape(selShape.id, { shadowBlur: v }),
      setShadowOffsetX: v => updateShape(selShape.id, { shadowOffsetX: v }),
      setShadowOffsetY: v => updateShape(selShape.id, { shadowOffsetY: v }),
      setShadowOpacity: v => updateShape(selShape.id, { shadowOpacity: v }),
      setReflectionEnabled: v => updateShape(selShape.id, { reflectionEnabled: v }),
    };
  } else if (selectedCanvasElement === 'image' && selImage) {
    ep = {
      opacity: selImage.opacity, blurRadius: selImage.blurRadius,
      shadowEnabled: selImage.shadowEnabled, shadowColor: selImage.shadowColor,
      shadowBlur: selImage.shadowBlur, shadowOffsetX: selImage.shadowOffsetX,
      shadowOffsetY: selImage.shadowOffsetY, shadowOpacity: selImage.shadowOpacity,
      reflectionEnabled: selImage.reflectionEnabled, filter: selImage.filter,
      setOpacity: v => updateImage(selImage.id, { opacity: v }),
      setBlurRadius: v => updateImage(selImage.id, { blurRadius: v }),
      setShadowEnabled: v => updateImage(selImage.id, { shadowEnabled: v }),
      setShadowColor: v => updateImage(selImage.id, { shadowColor: v }),
      setShadowBlur: v => updateImage(selImage.id, { shadowBlur: v }),
      setShadowOffsetX: v => updateImage(selImage.id, { shadowOffsetX: v }),
      setShadowOffsetY: v => updateImage(selImage.id, { shadowOffsetY: v }),
      setShadowOpacity: v => updateImage(selImage.id, { shadowOpacity: v }),
      setReflectionEnabled: v => updateImage(selImage.id, { reflectionEnabled: v }),
    };
  } else if (selectedCanvasElement === 'sticker' && selSticker) {
    ep = {
      opacity: selSticker.opacity, blurRadius: 0, // Stickers no tienen blur
      shadowEnabled: false, shadowColor: '#000000', shadowBlur: 0,
      shadowOffsetX: 0, shadowOffsetY: 0, shadowOpacity: 0,
      reflectionEnabled: false, filter: 'none',
      setOpacity: v => updateSticker(selSticker.id, { opacity: v }),
      setBlurRadius: () => { },
      setShadowEnabled: () => { }, setShadowColor: () => { },
      setShadowBlur: () => { }, setShadowOffsetX: () => { },
      setShadowOffsetY: () => { }, setShadowOpacity: () => { },
      setReflectionEnabled: () => { },
    };
  } else if (selectedCanvasElement === 'date' && dateElement) {
    ep = {
      opacity: dateElement.opacity, blurRadius: dateElement.blurRadius,
      shadowEnabled: dateElement.shadowEnabled, shadowColor: dateElement.shadowColor,
      shadowBlur: dateElement.shadowBlur, shadowOffsetX: dateElement.shadowOffsetX,
      shadowOffsetY: dateElement.shadowOffsetY, shadowOpacity: dateElement.shadowOpacity,
      reflectionEnabled: dateElement.reflectionEnabled, filter: dateElement.filter,
      setOpacity: v => updateDate({ opacity: v }),
      setBlurRadius: v => updateDate({ blurRadius: v }),
      setShadowEnabled: v => updateDate({ shadowEnabled: v }),
      setShadowColor: v => updateDate({ shadowColor: v }),
      setShadowBlur: v => updateDate({ shadowBlur: v }),
      setShadowOffsetX: v => updateDate({ shadowOffsetX: v }),
      setShadowOffsetY: v => updateDate({ shadowOffsetY: v }),
      setShadowOpacity: v => updateDate({ shadowOpacity: v }),
      setReflectionEnabled: v => updateDate({ reflectionEnabled: v }),
    };
  }

  // Paneles de UI
  const PropertiesPanel = () => (
    <div className="space-y-4">
      {!ep && (
        <div className="text-center py-8 text-gray-400">
          <Layers size={32} className="mx-auto mb-2 opacity-40" />
          <p className="text-sm">Selecciona un elemento para editarlo</p>
        </div>
      )}

      {selectedCanvasElement === 'text' && selText && (
        <div className="space-y-3">
          <SecHead title="Texto" icon={<Type size={12} />} />
          <textarea
            value={selText.text}
            onChange={(e) => { updateText(selText.id, { text: e.target.value }); }}
            onBlur={onTransformEndCommit}
            className="w-full text-sm border border-gray-200 rounded-lg p-2 resize-none focus:ring-1 focus:ring-blue-400 outline-none"
            rows={2}
          />
          <div className="grid grid-cols-2 gap-2">
            <div>
              <label className="text-xs text-gray-500 mb-1 block">Fuente</label>
              <select value={selText.fontFamily}
                onChange={(e) => { updateText(selText.id, { fontFamily: e.target.value }); onTransformEndCommit(); }}
                className="w-full text-xs border border-gray-200 rounded p-1.5 outline-none">
                {['Arial', 'Georgia', 'Impact', 'Courier New', 'Verdana', 'Times New Roman', 'Trebuchet MS'].map(f => <option key={f}>{f}</option>)}
              </select>
            </div>
            <div>
              <label className="text-xs text-gray-500 mb-1 block">Tamaño</label>
              <input type="number" min={8} max={200} value={selText.fontSize}
                onChange={(e) => { updateText(selText.id, { fontSize: Number(e.target.value) }); }}
                onBlur={onTransformEndCommit}
                className="w-full text-xs border border-gray-200 rounded p-1.5 outline-none" />
            </div>
          </div>
          <div className="flex items-center gap-2">
            <input type="color" value={selText.fill}
              onChange={(e) => { updateText(selText.id, { fill: e.target.value }); onTransformEndCommit(); }}
              className="w-8 h-8 rounded cursor-pointer border-0 p-0" title="Color de texto" />
            <span className="text-xs text-gray-500">Color</span>
            <div className="flex gap-1 ml-auto">
              {(['left', 'center', 'right'] as const).map(a => (
                <button key={a} onClick={() => { updateText(selText.id, { align: a }); onTransformEndCommit(); }}
                  className={`p-1.5 rounded ${selText.align === a ? 'bg-blue-100 text-blue-600' : 'text-gray-400 hover:bg-gray-100'}`}>
                  {a === 'left' ? <AlignLeft size={14} /> : a === 'center' ? <AlignCenter size={14} /> : <AlignRight size={14} />}
                </button>
              ))}
            </div>
          </div>
          <div className="flex gap-1">
            <button onClick={() => { updateText(selText.id, { textDecoration: selText.textDecoration === 'bold' ? 'none' : 'bold' }); onTransformEndCommit(); }}
              className={`flex-1 py-1.5 rounded text-xs font-medium ${selText.textDecoration === 'bold' ? 'bg-blue-100 text-blue-600' : 'bg-gray-100 text-gray-600'}`}>
              <Bold size={13} className="mx-auto" />
            </button>
            <button onClick={() => { updateText(selText.id, { fontStyle: selText.fontStyle === 'italic' ? 'normal' : 'italic' }); onTransformEndCommit(); }}
              className={`flex-1 py-1.5 rounded text-xs font-medium ${selText.fontStyle === 'italic' ? 'bg-blue-100 text-blue-600' : 'bg-gray-100 text-gray-600'}`}>
              <Italic size={13} className="mx-auto" />
            </button>
            <button onClick={() => { updateText(selText.id, { textDecoration: selText.textDecoration === 'underline' ? 'none' : 'underline' }); onTransformEndCommit(); }}
              className={`flex-1 py-1.5 rounded text-xs font-medium ${selText.textDecoration === 'underline' ? 'bg-blue-100 text-blue-600' : 'bg-gray-100 text-gray-600'}`}>
              <Underline size={13} className="mx-auto" />
            </button>
          </div>
        </div>
      )}

      {selectedCanvasElement === 'shape' && selShape && (
        <div className="space-y-3">
          <SecHead title="Forma" icon={<Square size={12} />} />
          <div className="grid grid-cols-2 gap-2">
            <div>
              <label className="text-xs text-gray-500 mb-1 block">Relleno</label>
              <input type="color" value={selShape.fill || '#000000'}
                onChange={(e) => { updateShape(selShape.id, { fill: e.target.value }); onTransformEndCommit(); }}
                className="w-full h-8 rounded cursor-pointer border border-gray-200 p-0.5" />
            </div>
            <div>
              <label className="text-xs text-gray-500 mb-1 block">Borde</label>
              <input type="color" value={selShape.stroke || '#000000'}
                onChange={(e) => { updateShape(selShape.id, { stroke: e.target.value }); onTransformEndCommit(); }}
                className="w-full h-8 rounded cursor-pointer border border-gray-200 p-0.5" />
            </div>
          </div>
          <Slider label="Grosor borde" value={selShape.strokeWidth} min={0} max={20}
            onChange={v => updateShape(selShape.id, { strokeWidth: v })} onCommit={onTransformEndCommit} />
        </div>
      )}

      {selectedCanvasElement === 'sticker' && selSticker && (
        <div className="space-y-3">
          <SecHead title="Sticker" icon={<Sparkles size={12} />} />
          <div className="text-center p-2 bg-gray-50 rounded">
            <span className="text-2xl font-black text-white px-3 py-1 rounded-full shadow-sm" style={{
              backgroundColor: selSticker.type === 'PROMO' ? '#ef4444' : selSticker.type === 'OFERTA' ? '#f59e0b' : selSticker.type === 'DESCUENTO' ? '#10b981' : selSticker.type === 'NEW' ? '#3b82f6' : '#8b5cf6'
            }}>
              {selSticker.type}
            </span>
          </div>
          <Slider label="Tamaño" value={Math.round(selSticker.scaleX * 100)} min={20} max={300} display={`${Math.round(selSticker.scaleX * 100)}%`}
            onChange={v => updateSticker(selSticker.id, { scaleX: v / 100, scaleY: v / 100 })} onCommit={onTransformEndCommit} />
        </div>
      )}

      {selectedCanvasElement === 'date' && dateElement && (
        <div className="space-y-3">
          <SecHead title="Fecha" icon={<Type size={12} />} />
          <select value={dateElement.format}
            onChange={(e) => { const f = e.target.value; updateDate({ format: f, text: formatDate(new Date(), f) }); onTransformEndCommit(); }}
            className="w-full text-xs border border-gray-200 rounded p-1.5 outline-none">
            {['DD/MM/YYYY', 'MMMM DD, YYYY', 'YYYY-MM-DD', 'DD MMMM YYYY'].map(f => <option key={f}>{f}</option>)}
          </select>
          <div className="flex items-center gap-2">
            <input type="color" value={dateElement.fill}
              onChange={(e) => { updateDate({ fill: e.target.value }); onTransformEndCommit(); }}
              className="w-8 h-8 rounded cursor-pointer border-0 p-0" />
            <span className="text-xs text-gray-500">Color</span>
          </div>
          <Slider label="Tamaño" value={dateElement.fontSize} min={8} max={120}
            onChange={v => updateDate({ fontSize: v })} onCommit={onTransformEndCommit} />
        </div>
      )}

      {ep && (
        <>
          <div className="space-y-3">
            <SecHead title="Apariencia" icon={<Sun size={12} />} />
            <Slider label="Opacidad" value={Math.round(ep.opacity * 100)} min={0} max={100}
              display={`${Math.round(ep.opacity * 100)}%`}
              onChange={v => ep!.setOpacity(v / 100)} onCommit={onTransformEndCommit} />
            {selectedCanvasElement !== 'sticker' && (
              <Slider label="Desenfoque" value={ep.blurRadius} min={0} max={40}
                onChange={ep.setBlurRadius} onCommit={onTransformEndCommit} />
            )}
          </div>

          <div className="space-y-2">
            <SecHead title="Voltear" />
            <div className="grid grid-cols-2 gap-2">
              <button onClick={() => handleFlip('x')}
                className="flex items-center justify-center gap-1.5 py-2 text-xs font-medium rounded-lg bg-gray-100 hover:bg-gray-200 text-gray-700">
                <FlipHorizontal size={14} /> Horizontal
              </button>
              <button onClick={() => handleFlip('y')}
                className="flex items-center justify-center gap-1.5 py-2 text-xs font-medium rounded-lg bg-gray-100 hover:bg-gray-200 text-gray-700">
                <FlipVertical size={14} /> Vertical
              </button>
            </div>
          </div>

          <div className="space-y-2">
            <SecHead title="Filtro" icon={<Contrast size={12} />} />
            <div className="grid grid-cols-3 gap-1.5">
              {(['none', 'grayscale', 'sepia'] as FilterType[]).map(f => (
                <button key={f} onClick={() => handleFilter(f)}
                  className={`py-1.5 text-xs rounded-lg font-medium transition-colors ${ep!.filter === f ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}`}>
                  {f === 'none' ? 'Ninguno' : f === 'grayscale' ? 'B&N' : 'Sepia'}
                </button>
              ))}
            </div>
          </div>

          {selectedCanvasElement !== 'sticker' && selectedCanvasElement !== 'background' && (
            <div className="space-y-2">
              <div className="flex items-center justify-between">
                <SecHead title="Sombra" icon={<Droplets size={12} />} />
                <Toggle label="" value={ep.shadowEnabled} onChange={v => { ep!.setShadowEnabled(v); onTransformEndCommit(); }} />
              </div>
              {ep.shadowEnabled && (
                <div className="space-y-2 pl-1">
                  <div className="flex items-center gap-2">
                    <input type="color" value={ep.shadowColor}
                      onChange={(e) => { ep!.setShadowColor(e.target.value); onTransformEndCommit(); }}
                      className="w-7 h-7 rounded cursor-pointer border-0 p-0" />
                    <span className="text-xs text-gray-500">Color sombra</span>
                  </div>
                  <Slider label="Intensidad" value={ep.shadowBlur} min={0} max={50}
                    onChange={ep.setShadowBlur} onCommit={onTransformEndCommit} />
                  <Slider label="Offset X" value={ep.shadowOffsetX} min={-30} max={30}
                    onChange={ep.setShadowOffsetX} onCommit={onTransformEndCommit} />
                  <Slider label="Offset Y" value={ep.shadowOffsetY} min={-30} max={30}
                    onChange={ep.setShadowOffsetY} onCommit={onTransformEndCommit} />
                  <Slider label="Opacidad" value={Math.round(ep.shadowOpacity * 100)} min={0} max={100}
                    display={`${Math.round(ep.shadowOpacity * 100)}%`}
                    onChange={v => ep!.setShadowOpacity(v / 100)} onCommit={onTransformEndCommit} />
                </div>
              )}
            </div>
          )}

          {selectedCanvasElement !== 'sticker' && (
            <Toggle label="Reflejo" value={ep.reflectionEnabled}
              onChange={v => { ep!.setReflectionEnabled(v); onTransformEndCommit(); }} />
          )}

          {selectedCanvasElement !== 'background' && (
            <div className="space-y-2">
              <SecHead title="Capas" icon={<Layers size={12} />} />
              <div className="grid grid-cols-2 gap-1.5">
                {([['top', 'Al frente'], ['bottom', 'Al fondo'], ['up', 'Adelante'], ['down', 'Atrás']] as const).map(([a, l]) => (
                  <button key={a} onClick={() => handleZOrder(a)}
                    className="py-1.5 text-xs rounded-lg bg-gray-100 hover:bg-gray-200 text-gray-600 font-medium">
                    {l}
                  </button>
                ))}
              </div>
            </div>
          )}

          <div className="grid grid-cols-2 gap-2 pt-1">
            <button onClick={handleDuplicate}
              className="flex items-center justify-center gap-1.5 py-2 text-xs font-medium rounded-lg bg-gray-100 hover:bg-gray-200 text-gray-700">
              <PlusSquare size={14} /> Duplicar
            </button>
            <button onClick={handleDeleteElement}
              className="flex items-center justify-center gap-1.5 py-2 text-xs font-medium rounded-lg bg-red-50 hover:bg-red-100 text-red-600">
              <Trash2 size={14} /> Eliminar
            </button>
          </div>
        </>
      )}
    </div>
  );

  const LeftPanelContent = () => (
    <div className="space-y-6">
      {/* Sección Tamaños */}
      <div>
        <SecHead title="Tamaños de Lienzo" icon={<Monitor size={12} />} />
        <div className="grid grid-cols-1 gap-2">
          {SIZE_PRESETS.map(p => (
            <button
              key={p.label}
              onClick={() => {
                setCanvasWidth(p.width);
                setCanvasHeight(p.height);
                // Re-centrar elementos si es necesario
                onTransformEndCommit();
              }}
              className={`flex items-center justify-between px-3 py-2 text-xs rounded-lg border transition-all ${canvasWidth === p.width && canvasHeight === p.height ? 'border-blue-500 bg-blue-50 text-blue-700' : 'border-gray-200 hover:bg-gray-50 text-gray-700'}`}
            >
              <span>{p.label}</span>
              <span className="text-gray-400">{p.width}x{p.height}</span>
            </button>
          ))}
        </div>
      </div>

      {/* Sección Fondos */}
      <div>
        <SecHead title="Color de fondo" icon={<Sparkles size={12} />} />
        <div className="flex flex-wrap gap-2 mb-3">
          {['#ffffff', '#000000', '#f3f4f6', '#1e3a5f', '#7c3aed', '#dc2626', 'transparent'].map(c => (
            <button key={c} onClick={() => {
              setCanvasBackgroundColor(c);
              setSelectedPresetBackgroundUrl(undefined);
              setSelectedCanvasElement('background');
              onTransformEndCommit();
            }}
              className={`w-7 h-7 rounded-full border-2 flex-shrink-0 ${canvasBackgroundColor === c ? 'border-blue-500' : 'border-transparent hover:border-gray-300'}`}
              style={{
                backgroundColor: c === 'transparent' ? undefined : c,
                backgroundImage: c === 'transparent'
                  ? 'linear-gradient(45deg,#ccc 25%,transparent 25%),linear-gradient(-45deg,#ccc 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#ccc 75%),linear-gradient(-45deg,transparent 75%,#ccc 75%)'
                  : undefined,
                backgroundSize: c === 'transparent' ? '8px 8px' : undefined,
                backgroundPosition: c === 'transparent' ? '0 0,0 4px,4px -4px,-4px 0' : undefined,
              }}
            />
          ))}
          <input type="color"
            value={canvasBackgroundColor.startsWith('#') && canvasBackgroundColor !== 'transparent' ? canvasBackgroundColor : '#ffffff'}
            onChange={(e) => { setCanvasBackgroundColor(e.target.value); setSelectedPresetBackgroundUrl(undefined); onTransformEndCommit(); }}
            className="w-7 h-7 rounded-full cursor-pointer border-0 p-0 overflow-hidden"
            title="Color personalizado"
          />
        </div>

        <div>
          <button onClick={() => bgFileRef.current?.click()}
            className="w-full py-2 text-xs font-medium rounded-lg border-2 border-dashed border-gray-300 hover:border-blue-400 hover:bg-blue-50 text-gray-500 hover:text-blue-600 transition-colors mb-3 flex items-center justify-center gap-1.5">
            <ImageIcon size={14} /> Subir imagen de fondo
          </button>
          <input type="file" accept="image/*" ref={bgFileRef} onChange={handleBgFileUpload} className="hidden" />
        </div>

        <div className="grid grid-cols-2 gap-2">
          {allBgs.map(bg => {
            const isColor = bg.url.startsWith('#');
            const isSelected = isColor ? canvasBackgroundColor === bg.url : selectedPresetBackgroundUrl === bg.url;
            return (
              <button key={bg.id} onClick={() => handleSelectBg(bg)}
                disabled={bg.isPremium && !isUserPremium}
                className={`relative rounded-lg overflow-hidden aspect-video transition-all ${isSelected ? 'ring-2 ring-blue-500 ring-offset-1' : 'hover:opacity-90'} ${bg.isPremium && !isUserPremium ? 'opacity-50' : ''}`}>
                {isColor
                  ? <div className="w-full h-full" style={{ backgroundColor: bg.url }} />
                  : <img src={bg.url} alt="" className="w-full h-full object-cover" loading="lazy" />
                }
                {isSelected && (
                  <div className="absolute inset-0 flex items-center justify-center bg-blue-500/20">
                    <Check size={16} className="text-white drop-shadow" />
                  </div>
                )}
                {bg.isPremium && !isUserPremium && (
                  <div className="absolute top-1 right-1 bg-amber-400 text-white text-[9px] font-bold px-1 rounded">PRO</div>
                )}
              </button>
            );
          })}
        </div>
      </div>

      {/* Sección Stickers */}
      <div>
        <SecHead title="Stickers" icon={<ShoppingBag size={12} />} />
        <div className="grid grid-cols-2 gap-2">
          {['PROMO', 'OFERTA', 'DESCUENTO', 'NEW', 'SALE', 'VIP'].map(type => (
            <button
              key={type}
              onClick={() => handleAddSticker(type as any)}
              className="flex items-center justify-center py-3 px-2 rounded-lg bg-white border border-gray-200 shadow-sm hover:bg-gray-50 active:bg-gray-100 transition-all"
            >
              <span
                className="text-xs font-black text-white px-2 py-1 rounded-full shadow-md"
                style={{
                  backgroundColor: type === 'PROMO' ? '#ef4444' : type === 'OFERTA' ? '#f59e0b' : type === 'DESCUENTO' ? '#10b981' : type === 'NEW' ? '#3b82f6' : type === 'SALE' ? '#8b5cf6' : '#ec4899'
                }}
              >
                {type}
              </span>
            </button>
          ))}
        </div>
      </div>
    </div>
  );

  // Render Principal
  return (
    <div className="h-screen flex flex-col bg-gray-50">
      {/* Header */}
      <header className="bg-white border-b border-gray-200 flex items-center gap-2 px-3 py-2 z-20 flex-shrink-0">
        <img src="https://www.editta.app/api/media/editta-logo-1-1776268088659.png"
          alt="Editta" className="h-7 mr-1 hidden sm:block" />

        <div className="flex gap-1 flex-shrink-0">
          <button onClick={handleUndo} disabled={historyPointer <= 0}
            className="p-1.5 rounded-lg disabled:opacity-40 disabled:cursor-not-allowed hover:bg-gray-100 text-gray-600 transition-colors" title="Deshacer (Ctrl+Z)">
            <Undo2 size={16} />
          </button>
          <button onClick={handleRedo} disabled={historyPointer >= history.length - 1}
            className="p-1.5 rounded-lg disabled:opacity-40 disabled:cursor-not-allowed hover:bg-gray-100 text-gray-600 transition-colors" title="Rehacer">
            <Redo2 size={16} />
          </button>
        </div>

        <div className="w-px h-5 bg-gray-200 mx-1 hidden sm:block" />

        {/* Tools */}
        <div className="flex items-center gap-1 overflow-x-auto flex-1 scrollbar-none">
          <button onClick={() => productFileRef.current?.click()}
            className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-blue-500 hover:bg-blue-600 text-white text-xs font-medium flex-shrink-0 transition-colors">
            <input type="file" accept="image/*" ref={productFileRef} onChange={handleProductUpload} className="hidden" />
            <ImageMinus size={14} />
            <span className="hidden sm:inline">Producto</span>
          </button>
          <button onClick={() => elementImageRef.current?.click()}
            className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg hover:bg-gray-100 text-gray-700 text-xs font-medium flex-shrink-0 transition-colors">
            <input type="file" accept="image/*" ref={elementImageRef} onChange={handleElementImageUpload} className="hidden" />
            <ImageIcon size={14} />
            <span className="hidden sm:inline">Imagen</span>
          </button>
          <button onClick={handleAddText}
            className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg hover:bg-gray-100 text-gray-700 text-xs font-medium flex-shrink-0 transition-colors">
            <Type size={14} />
            <span className="hidden sm:inline">Texto</span>
          </button>
          <button onClick={() => { setLeftPanel('stickers'); setMobilePanel('left'); }}
            className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg hover:bg-gray-100 text-gray-700 text-xs font-medium flex-shrink-0 transition-colors">
            <ShoppingBag size={14} />
            <span className="hidden sm:inline">Stickers</span>
          </button>
          <button onClick={() => setShowCenterGuides(p => !p)}
            className={`p-1.5 rounded-lg flex-shrink-0 transition-colors ${showCenterGuides ? 'bg-blue-100 text-blue-600' : 'hover:bg-gray-100 text-gray-500'}`}
            title="Guías de centro">
            <Ruler size={14} />
          </button>
        </div>

        <div className="flex items-center gap-2 flex-shrink-0 ml-auto">
          {/* Mobile: toggle paneles */}
          <button onClick={() => setMobilePanel(p => p === 'left' ? 'none' : 'left')}
            className="sm:hidden p-1.5 rounded-lg hover:bg-gray-100 text-gray-600">
            <Layout size={16} />
          </button>
          <button onClick={() => setMobilePanel(p => p === 'right' ? 'none' : 'right')}
            className="sm:hidden p-1.5 rounded-lg hover:bg-gray-100 text-gray-600">
            <PanelLeft size={16} style={{ transform: 'scaleX(-1)' }} />
          </button>
          <button onClick={handleDownload}
            className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-green-500 hover:bg-green-600 text-white text-xs font-semibold transition-colors">
            <Download size={14} />
            <span>Descargar</span>
          </button>
        </div>
      </header>

      {/* Cuerpo */}
      <div className="flex flex-1 overflow-hidden relative">

        {/* Panel izquierdo — Desktop */}
        <aside className="hidden sm:flex flex-col w-64 bg-white border-r border-gray-200 overflow-hidden flex-shrink-0">
          <div className="px-3 py-2.5 border-b border-gray-100 flex items-center gap-2">
            <button onClick={() => setLeftPanel('backgrounds')} className={`text-xs font-semibold ${leftPanel === 'backgrounds' ? 'text-blue-600' : 'text-gray-500'}`}>Fondos</button>
            <button onClick={() => setLeftPanel('stickers')} className={`text-xs font-semibold ${leftPanel === 'stickers' ? 'text-blue-600' : 'text-gray-500'}`}>Stickers</button>
            <button onClick={() => setLeftPanel('sizes')} className={`text-xs font-semibold ${leftPanel === 'sizes' ? 'text-blue-600' : 'text-gray-500'}`}>Tamaños</button>
          </div>
          <div className="flex-1 overflow-y-auto p-3">
            {leftPanel === 'backgrounds' && <LeftPanelContent />}
            {leftPanel === 'stickers' && (
              <div className="grid grid-cols-2 gap-2">
                {['PROMO', 'OFERTA', 'DESCUENTO', 'NEW', 'SALE', 'VIP'].map(type => (
                  <button key={type} onClick={() => handleAddSticker(type as any)} className="flex items-center justify-center py-4 px-2 rounded-lg bg-white border border-gray-200 shadow-sm hover:bg-gray-50">
                    <span className="text-xs font-black text-white px-2 py-1 rounded-full shadow-md" style={{
                      backgroundColor: type === 'PROMO' ? '#ef4444' : type === 'OFERTA' ? '#f59e0b' : type === 'DESCUENTO' ? '#10b981' : type === 'NEW' ? '#3b82f6' : type === 'SALE' ? '#8b5cf6' : '#ec4899'
                    }}>{type}</span>
                  </button>
                ))}
              </div>
            )}
            {leftPanel === 'sizes' && (
              <div className="space-y-2">
                {SIZE_PRESETS.map(p => (
                  <button key={p.label} onClick={() => { setCanvasWidth(p.width); setCanvasHeight(p.height); onTransformEndCommit(); }}
                    className={`w-full text-left px-3 py-2 text-xs rounded-lg border ${canvasWidth === p.width && canvasHeight === p.height ? 'border-blue-500 bg-blue-50 text-blue-700' : 'border-gray-200 hover:bg-gray-50'}`}>
                    <span className="block font-medium">{p.label}</span>
                    <span className="block text-gray-400 mt-1">{p.width} x {p.height} px</span>
                  </button>
                ))}
              </div>
            )}
          </div>
        </aside>

        {/* Canvas */}
        <main className="flex-1 flex items-center justify-center p-4 overflow-hidden bg-gray-100">
          <div style={{
            transform: `scale(${Math.min(
              1,
              typeof window !== 'undefined'
                ? Math.min(
                  (window.innerWidth - (window.innerWidth > 640 ? 512 : 16) - 32) / canvasWidth,
                  (window.innerHeight - 80 - 32) / canvasHeight
                )
                : 1
            )})`,
            transformOrigin: 'center center',
          }}>
            <DynamicCanvasEditor
              canvasWidth={canvasWidth}
              canvasHeight={canvasHeight}
              productImageUrl={productImageUrl}
              backgroundUrl={selectedPresetBackgroundUrl}
              canvasBackgroundColor={canvasBackgroundColor}
              productX={productX} productY={productY}
              productScale={productScale} productRotation={productRotation}
              productOpacity={productOpacity} productBlurRadius={productBlurRadius}
              productShadowEnabled={productShadowEnabled}
              productShadowColor={productShadowColor} productShadowBlur={productShadowBlur}
              productShadowOffsetX={productShadowOffsetX} productShadowOffsetY={productShadowOffsetY}
              productShadowOpacity={productShadowOpacity}
              productReflectionEnabled={productReflectionEnabled}
              productFlipX={productFlipX} productFlipY={productFlipY} productFilter={productFilter}
              setProductX={setProductX} setProductY={setProductY} setProductScale={setProductScale}
              setHasProductBeenScaledManually={setHasProductBeenScaledManually}
              onProductRotate={setProductRotation}
              backgroundOpacity={backgroundOpacity} backgroundBlurRadius={backgroundBlurRadius}
              backgroundShadowEnabled={backgroundShadowEnabled}
              backgroundShadowColor={backgroundShadowColor} backgroundShadowBlur={backgroundShadowBlur}
              backgroundShadowOffsetX={backgroundShadowOffsetX} backgroundShadowOffsetY={backgroundShadowOffsetY}
              backgroundShadowOpacity={backgroundShadowOpacity}
              backgroundReflectionEnabled={backgroundReflectionEnabled}
              backgroundFlipX={backgroundFlipX} backgroundFlipY={backgroundFlipY} backgroundFilter={backgroundFilter}
              selectedCanvasElement={selectedCanvasElement}
              selectedElementId={selectedElementId}
              onElementSelect={handleElementSelect}
              textElements={textElements} onTextUpdate={updateText}
              shapeElements={shapeElements} onShapeUpdate={updateShape}
              dateElement={dateElement} onDateUpdate={updateDate}
              imageElements={imageElements} onImageUpdate={updateImage}
              onImageAddedAndLoaded={handleImageAddedAndLoaded}
              stickerElements={stickerElements} onStickerUpdate={updateSticker}
              onTransformEndCommit={onTransformEndCommit}
              onCanvasReady={setKonvaCanvas}
              showCenterGuides={showCenterGuides}
              zOrderAction={zOrderAction}
              onZOrderActionComplete={onZOrderActionComplete}
              imageToMoveToBottomId={imageToMoveToBottomId}
              onImageMovedToBottom={onImageMovedToBottom}
            />
          </div>
        </main>

        {/* Panel derecho — Desktop */}
        <aside className="hidden sm:flex flex-col w-72 bg-white border-l border-gray-200 overflow-hidden flex-shrink-0">
          <div className="px-3 py-2.5 border-b border-gray-100">
            <span className="text-xs font-semibold text-gray-500 uppercase tracking-wider">Propiedades</span>
          </div>
          <div className="flex-1 overflow-y-auto p-4">
            <PropertiesPanel />
          </div>
        </aside>

        {/* Paneles móviles (overlay limpio) */}
        {mobilePanel !== 'none' && (
          <div className="sm:hidden absolute inset-0 z-30 flex">
            <div className="absolute inset-0 bg-black/40" onClick={() => setMobilePanel('none')} />
            <div className={`relative bg-white h-full w-full sm:w-80 flex flex-col shadow-2xl overflow-hidden ${mobilePanel === 'right' ? 'ml-auto' : ''}`}>
              <div className="flex items-center justify-between px-4 py-3 border-b border-gray-100 flex-shrink-0 bg-gray-50">
                <span className="text-sm font-semibold text-gray-700">
                  {mobilePanel === 'left' ? 'Herramientas' : 'Propiedades'}
                </span>
                <button onClick={() => setMobilePanel('none')} className="p-2 rounded-full hover:bg-gray-200">
                  <X size={18} />
                </button>
              </div>
              <div className="flex-1 overflow-y-auto p-4">
                {mobilePanel === 'left' ? <LeftPanelContent /> : <PropertiesPanel />}
              </div>
            </div>
          </div>
        )}
      </div>

      {/* Barra inferior móvil (Estilo PhotoRoom) */}
      <nav className="sm:hidden bg-white border-t border-gray-200 flex items-center justify-around px-2 py-2 z-20 flex-shrink-0 safe-area-pb">
        <button onClick={() => setMobilePanel(p => p === 'left' ? 'none' : 'left')}
          className={`flex flex-col items-center gap-1 p-2 rounded-lg ${mobilePanel === 'left' ? 'text-blue-600' : 'text-gray-500'}`}>
          <Layout size={20} />
          <span className="text-[10px] font-medium">Fondos</span>
        </button>
        <button onClick={handleAddText}
          className="flex flex-col items-center gap-1 p-2 rounded-lg text-gray-500">
          <Type size={20} />
          <span className="text-[10px] font-medium">Texto</span>
        </button>
        <button onClick={() => { setLeftPanel('stickers'); setMobilePanel('left'); }}
          className="flex flex-col items-center gap-1 p-2 rounded-lg text-gray-500">
          <ShoppingBag size={20} />
          <span className="text-[10px] font-medium">Stickers</span>
        </button>
        <button onClick={() => setMobilePanel(p => p === 'right' ? 'none' : 'right')}
          className={`flex flex-col items-center gap-1 p-2 rounded-lg ${mobilePanel === 'right' ? 'text-blue-600' : 'text-gray-500'}`}>
          <PanelLeft size={20} style={{ transform: 'scaleX(-1)' }} />
          <span className="text-[10px] font-medium">Editar</span>
        </button>
        <button onClick={handleDownload}
          className="flex flex-col items-center gap-1 p-2 rounded-lg text-green-600">
          <Download size={20} />
          <span className="text-[10px] font-medium">Guardar</span>
        </button>
      </nav>
    </div>
  );
}
</file>

<file path="src/components/datatable/filters.tsx">
import { ListFilterIcon } from 'lucide-react';
import React from 'react';

import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';

import { Button } from '../ui/button';
import { Label } from '../ui/label';
import { MultiSelect } from '../ui/multi-select';
import { Filter } from './types';

const TableFilters = ({ filters }: { filters: Filter[] }) => {
  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button variant="outline" className="h-9 relative">
          <ListFilterIcon />
          Filters
          {filters.some((filter) => filter.value.length > 0) && (
            <span className="absolute top-0 right-0 -mt-1 -mr-1 flex items-center justify-center h-2 w-2 rounded-full bg-primary text-white text-xs">
              <span className="animate-ping absolute inline-flex h-2 w-2 rounded-full bg-primary opacity-75"></span>
              <span className="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
            </span>
          )}
        </Button>
      </PopoverTrigger>
      <PopoverContent side="bottom" align="end" className="space-y-4 p-5">
        <div>
          <h3 className="text-lg font-bold mb-1">Filters</h3>
          <hr />
        </div>
        {filters.map((filter) => {
          if (filter.type === 'multi-select') {
            return (
              <div key={filter.key} className="space-y-2">
                <Label>{filter.label}</Label>
                <MultiSelect
                  value={filter.value}
                  placeholder={`Filter by ${filter.label.toLowerCase()}`}
                  onValueChange={filter.onFilter}
                  className="min-h-9 px-0"
                  options={filter.options}
                  maxCount={1}
                />
              </div>
            );
          }

          return null;
        })}
        {filters.some((filter) => filter.value.length > 0) && (
          <Button
            size="sm"
            onClick={() => {
              filters.forEach((filter) => {
                filter.onFilter([]);
              });
            }}
          >
            Clear Filters
          </Button>
        )}
      </PopoverContent>
    </Popover>
  );
};

export default TableFilters;
</file>

<file path="src/components/datatable/index.tsx">
'use client';

import { InboxIcon, PlusIcon } from 'lucide-react';
import { ArrowDownIcon, ArrowUpIcon } from 'lucide-react';
import React from 'react';

import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';

import { cn } from '@/lib/utils';

import TableSkeletons from '../skeletons/table-skeleton';
import { Button } from '../ui/button';
import { Checkbox } from '../ui/checkbox';
import TableFilters from './filters';
import Pagination from './pagination';
import SearchFilter from './search-filter';
import { DataTableProps } from './types';

const DataTable = (props: DataTableProps) => {
  const {
    title,
    onSearch,
    addButtonText,
    onAddClick,
    pagination,
    isLoading,
    sort,
    data,
    columns,
    selection,
    onClickRow,
    actions,
    filters,
  } = props;

  return (
    <div>
      <h1 className="font-bold text-2xl">{title}</h1>
      <div className="mt-4 flex flex-col sm:flex-row items-center justify-between gap-5 sm:gap-2">
        <div className="flex-1 w-full sm:w-auto flex items-center gap-4">
          {onSearch && <SearchFilter onChange={onSearch} />}
        </div>
        <div className="flex gap-4 items-center w-full sm:w-auto">
          {filters && <TableFilters filters={filters} />}
          {addButtonText && (
            <Button className="h-9" onClick={onAddClick}>
              <PlusIcon /> {addButtonText}
            </Button>
          )}
        </div>
      </div>
      <div className="bg-card rounded-lg border mt-5 overflow-auto h-[calc(100svh-210px)] relative">
        {!data?.length && isLoading ? (
          <TableSkeletons />
        ) : (
          <Table>
            <TableHeader>
              <TableRow>
                {selection && (
                  <TableHead style={{ width: '40px' }}>
                    <Checkbox
                      checked={selection.selected.length > 0}
                      isIndeterminate={selection.selected.length !== data.length}
                      onCheckedChange={() => {
                        selection.setSelected(
                          selection.selected.length !== data.length
                            ? data.map((item) => item.id)
                            : [],
                        );
                      }}
                    />
                  </TableHead>
                )}
                {columns.map((column) => (
                  <TableHead
                    style={{
                      ...(column.width && { width: column.width }),
                      ...(column.minWidth && { minWidth: column.minWidth }),
                      ...(column.maxWidth && { maxWidth: column.maxWidth }),
                      ...(column.align && { textAlign: column.align }),
                    }}
                    key={column.key}
                    onClick={() => {
                      if (column.sortable) {
                        if (sort?.key === column.key) {
                          sort?.onSort(
                            sort.order === 'asc' ? undefined : column.key,
                            sort.order === 'asc'
                              ? undefined
                              : sort.order === 'desc'
                                ? 'asc'
                                : 'desc',
                          );
                        } else {
                          sort?.onSort(column.key, 'desc');
                        }
                      }
                    }}
                    className={cn('whitespace-nowrap group', {
                      'cursor-pointer': column.sortable,
                    })}
                  >
                    <div className="flex items-center gap-1.5">
                      {column.title}
                      {column.sortable && sort && (
                        <div className="inline-flex items-center relative size-4">
                          <ArrowUpIcon
                            className={cn('size-3.5 opacity-0 invisible transition absolute', {
                              'visible opacity-100':
                                sort.key === column.key && sort.order === 'asc',
                            })}
                          />
                          <ArrowDownIcon
                            className={cn('size-3.5 opacity-0 transition invisible absolute', {
                              'visible opacity-100':
                                sort.key === column.key && sort.order === 'desc',
                              'group-hover:opacity-40 group-hover:visible': sort.key !== column.key,
                            })}
                          />
                        </div>
                      )}
                    </div>
                  </TableHead>
                ))}
              </TableRow>
            </TableHeader>
            <TableBody>
              {data.map((row, index) => (
                <TableRow
                  key={index}
                  onClick={() => onClickRow?.(row)}
                  className={cn({ 'cursor-pointer': onClickRow })}
                >
                  {selection && (
                    <TableCell>
                      <Checkbox
                        onClick={(e) => {
                          e.preventDefault();
                          e.stopPropagation();

                          const isSelected = selection.selected.includes(row.id);
                          if (isSelected) {
                            selection.setSelected(selection.selected.filter((id) => id !== row.id));
                          } else {
                            selection.setSelected([...selection.selected, row.id]);
                          }
                        }}
                        checked={selection.selected.includes(row.id)}
                      />
                    </TableCell>
                  )}
                  {columns.map((column) => (
                    <TableCell
                      style={{
                        ...(column.width && { width: column.width }),
                        ...(column.minWidth && { minWidth: column.minWidth }),
                        ...(column.maxWidth && { maxWidth: column.maxWidth }),
                        ...(column.align && { textAlign: column.align }),
                      }}
                      key={column.key}
                    >
                      {column.render ? column.render(row[column.key], row) : row[column.key]}
                    </TableCell>
                  ))}
                </TableRow>
              ))}
              {data.length === 0 && (
                <TableRow className="!bg-transparent">
                  <TableCell colSpan={columns.length + (selection ? 1 : 0)}>
                    <div className="flex items-center flex-col justify-center min-h-[calc(100svh-287px)]">
                      <InboxIcon className="h-12 w-12 text-zinc-400 dark:text-zinc-600 stroke-[1.3]" />
                      <p className="text-zinc-400 dark:text-zinc-600 mt-2 text-md font-medium">
                        No data found
                      </p>
                    </div>
                  </TableCell>
                </TableRow>
              )}
            </TableBody>
          </Table>
        )}
        {selection && (
          <div
            className={cn(
              'absolute bottom-0 left-1/2 rounded-full min-w-[330px] -translate-x-1/2 opacity-0 transition-all invisible bg-card shadow-lg border border-border px-4 py-2 flex items-center justify-between',
              {
                'opacity-100 visible -translate-y-5': selection.selected.length > 0,
              },
            )}
          >
            <div className="flex items-center gap-2 mr-14">
              <p className="text-[13px]">
                <span>Selected</span> <strong>{selection.selected.length}</strong> <span>Rows</span>
              </p>
              <Button
                variant="outline"
                className="h-5 w-11 text-xs rounded-sm border-foreground/50"
                onClick={() => selection.setSelected([])}
              >
                Reset
              </Button>
            </div>
            <div className="flex items-center gap-2">
              {actions?.map((action, i) => (
                <Button
                  key={i}
                  className={cn('size-7 rounded-sm', action.className)}
                  onClick={action.onClick}
                  variant="outline"
                >
                  {action.label}
                </Button>
              ))}
            </div>
          </div>
        )}
      </div>
      <Pagination
        page={pagination.page}
        totalPages={pagination.totalPages}
        setPage={pagination.setPage}
        setLimit={pagination.setLimit}
        limit={pagination.limit}
      />
    </div>
  );
};

export default DataTable;
</file>

<file path="src/components/datatable/pagination.tsx">
import {
  ChevronLeftIcon,
  ChevronRightIcon,
  DoubleArrowLeftIcon,
  DoubleArrowRightIcon,
} from '@radix-ui/react-icons';

import { Select } from '@/components/ui/select';

import { Button } from '../ui/button';

interface PaginationProps {
  totalPages: number;
  page: number;
  setPage: (_page: number) => void;
  limit: number;
  setLimit: (_limit: number) => void;
}

function Pagination({ totalPages, page, setPage, limit, setLimit }: PaginationProps) {
  return (
    <div className="mt-5 flex items-center justify-between">
      <p className="hidden text-muted-foreground lg:block">
        Showing {page} of {totalPages} pages
      </p>
      <div className="flex w-full items-center justify-between gap-6 lg:w-auto">
        <div className="flex items-center gap-2">
          <p className="hidden lg:block">Rows per page</p>
          <Select
            className="w-[80px] h-9 border-border dark:border-input/60"
            value={limit}
            onChange={(e) => setLimit(parseInt(e.target.value, 10))}
          >
            <option value="5">5</option>
            <option value="10">10</option>
            <option value="15">15</option>
            <option value="50">50</option>
            <option value="100">100</option>
          </Select>
        </div>
        <div className="flex items-center gap-1.5">
          <Button
            variant="outline"
            className="bg-card size-9 disabled:opacity-50"
            size="icon"
            disabled={page === 1}
            onClick={() => setPage(page - 1)}
          >
            <DoubleArrowLeftIcon />
          </Button>
          <Button
            variant="outline"
            size="icon"
            className="bg-card size-9 disabled:opacity-50"
            disabled={page === 1}
            onClick={() => setPage(page - 1)}
          >
            <ChevronLeftIcon />
          </Button>
          <Button
            variant="outline"
            size="icon"
            className="bg-card size-9 disabled:opacity-50"
            disabled={page >= totalPages}
            onClick={() => setPage(page + 1)}
          >
            <ChevronRightIcon />
          </Button>
          <Button
            variant="outline"
            size="icon"
            className="bg-card size-9 disabled:opacity-50"
            disabled={page >= totalPages}
            onClick={() => setPage(totalPages)}
          >
            <DoubleArrowRightIcon />
          </Button>
        </div>
      </div>
    </div>
  );
}

export default Pagination;
</file>

<file path="src/components/datatable/search-filter.tsx">
import { SearchIcon } from 'lucide-react';
import React, { useState, useEffect } from 'react';
import { useDebounce } from 'use-debounce';

import { Input } from '../ui/input';

const SearchFilter = ({
  onChange,
  debounceTime = 500,
}: {
  onChange: (value: string) => void;
  debounceTime?: number;
}) => {
  const [value, setValue] = useState('');
  const [debouncedValue] = useDebounce(value, debounceTime);

  useEffect(() => {
    onChange(debouncedValue);
  }, [debouncedValue]);

  return (
    <div className="flex items-center relative w-full max-w-[350px]">
      <SearchIcon className="size-4 text-muted-foreground/70 absolute left-3" />
      <span className="sr-only">Search</span>
      <Input
        type="search"
        placeholder="Search..."
        className="pl-9 h-9"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
    </div>
  );
};

export default SearchFilter;
</file>

<file path="src/components/datatable/types.ts">
import { JSX } from 'react';

type Column = {
  title: string;
  key: string;
  render?: (value: any, record: any) => JSX.Element;
  width?: number;
  maxWidth?: number;
  minWidth?: number;
  align?: 'left' | 'right' | 'center';
  sortable?: boolean;
};

type MultiSelectFilter = {
  type: 'multi-select';
  key: string;
  label: string;
  options: { label: string; value: string }[];
  value: string[];
  onFilter: (value: string[]) => void;
};

export type Filter = MultiSelectFilter;

export type DataTableProps = {
  title: string;
  onSearch?: (value: string) => void;
  addButtonText?: string;
  onAddClick?: () => void;
  selection?: {
    selected: string[];
    setSelected: (selected: string[]) => void;
  };
  pagination: {
    page: number;
    totalPages: number;
    setPage: (page: number) => void;
    setLimit: (limit: number) => void;
    limit: number;
  };
  filters?: Filter[];
  sort?: {
    key?: string;
    order?: 'asc' | 'desc';
    onSort: (key?: string, order?: 'asc' | 'desc') => void;
  };
  onClickRow?: (record: any) => void;
  isLoading?: boolean;
  columns: Column[];
  data: any[];
  actions?: {
    label: JSX.Element;
    className?: string;
    onClick: () => void;
  }[];
};
</file>

<file path="src/components/rich-editor/components/bubble-menu/link-bubble-menu.tsx">
import type { Editor } from '@tiptap/react';
import { BubbleMenu } from '@tiptap/react';
import * as React from 'react';

import type { ShouldShowProps } from '../../types';
import { LinkEditBlock } from '../link/link-edit-block';
import { LinkPopoverBlock } from '../link/link-popover-block';

interface LinkBubbleMenuProps {
  editor: Editor;
}

interface LinkAttributes {
  href: string;
  target: string;
}

export const LinkBubbleMenu: React.FC<LinkBubbleMenuProps> = ({ editor }) => {
  const [showEdit, setShowEdit] = React.useState(false);
  const [linkAttrs, setLinkAttrs] = React.useState<LinkAttributes>({ href: '', target: '' });
  const [selectedText, setSelectedText] = React.useState('');

  const updateLinkState = React.useCallback(() => {
    const { from, to } = editor.state.selection;
    const { href, target } = editor.getAttributes('link');
    const text = editor.state.doc.textBetween(from, to, ' ');

    setLinkAttrs({ href, target });
    setSelectedText(text);
  }, [editor]);

  const shouldShow = React.useCallback(
    ({ editor, from, to }: ShouldShowProps) => {
      if (from === to) {
        return false;
      }
      const { href } = editor.getAttributes('link');

      if (!editor.isActive('link') || !editor.isEditable) {
        return false;
      }

      if (href) {
        updateLinkState();

        return true;
      }

      return false;
    },
    [updateLinkState],
  );

  const handleEdit = React.useCallback(() => {
    setShowEdit(true);
  }, []);

  const onSetLink = React.useCallback(
    (url: string, text?: string, openInNewTab?: boolean) => {
      editor
        .chain()
        .focus()
        .extendMarkRange('link')
        .insertContent({
          type: 'text',
          text: text || url,
          marks: [
            {
              type: 'link',
              attrs: {
                href: url,
                target: openInNewTab ? '_blank' : '',
              },
            },
          ],
        })
        .setLink({ href: url, target: openInNewTab ? '_blank' : '' })
        .run();
      setShowEdit(false);
      updateLinkState();
    },
    [editor, updateLinkState],
  );

  const onUnsetLink = React.useCallback(() => {
    editor.chain().focus().extendMarkRange('link').unsetLink().run();
    setShowEdit(false);
    updateLinkState();
  }, [editor, updateLinkState]);

  return (
    <BubbleMenu
      editor={editor}
      shouldShow={shouldShow}
      tippyOptions={{
        placement: 'bottom-start',
        onHidden: () => setShowEdit(false),
      }}
    >
      {showEdit ? (
        <LinkEditBlock
          defaultUrl={linkAttrs.href}
          defaultText={selectedText}
          defaultIsNewTab={linkAttrs.target === '_blank'}
          onSave={onSetLink}
          className="w-full min-w-80 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none"
        />
      ) : (
        <LinkPopoverBlock onClear={onUnsetLink} url={linkAttrs.href} onEdit={handleEdit} />
      )}
    </BubbleMenu>
  );
};
</file>

<file path="src/components/rich-editor/components/image/image-edit-block.tsx">
import MediaTable from '@/app/admin/media/components/media-table';
import type { Editor } from '@tiptap/react';
import * as React from 'react';

import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';

interface ImageEditBlockProps {
  editor: Editor;
  close: () => void;
}

export const ImageEditBlock: React.FC<ImageEditBlockProps> = ({ editor, close }) => {
  const [link, setLink] = React.useState('');
  const [openDialog, setOpenDialog] = React.useState(false);

  const handleSubmit = React.useCallback(
    (e: React.FormEvent<HTMLFormElement>) => {
      e.preventDefault();
      e.stopPropagation();

      if (link) {
        editor.commands.setImage({ src: link });
        close();
      }
    },
    [editor, link, close],
  );

  return (
    <form onSubmit={handleSubmit} className="space-y-8">
      <div className="space-y-2">
        <Label htmlFor="image-link">Attach an image link</Label>
        <div className="flex">
          <Input
            id="image-link"
            type="url"
            required
            placeholder="https://example.com"
            value={link}
            className="grow"
            onChange={(e: React.ChangeEvent<HTMLInputElement>) => setLink(e.target.value)}
          />
          <Button type="submit" className="ml-2 h-10">
            Submit
          </Button>
        </div>
      </div>
      <Button type="button" className="w-full h-10" onClick={() => setOpenDialog(true)}>
        Select from media
      </Button>
      <Dialog open={openDialog} onOpenChange={() => setOpenDialog(false)}>
        <DialogContent className="sm:max-w-5xl">
          <div>
            <DialogHeader>
              <DialogTitle className="sr-only">Select Image</DialogTitle>
            </DialogHeader>
            <div className="w-full">
              <MediaTable
                onSelect={(e) => {
                  setOpenDialog(false);
                  close();
                  if (!e) return;
                  editor.commands.setImage({ src: e });
                }}
                allowTypes={['image']}
              />
            </div>
          </div>
        </DialogContent>
      </Dialog>
    </form>
  );
};

export default ImageEditBlock;
</file>

<file path="src/components/rich-editor/components/image/image-edit-dialog.tsx">
import { ImageIcon } from '@radix-ui/react-icons';
import type { Editor } from '@tiptap/react';
import type { VariantProps } from 'class-variance-authority';
import { useState } from 'react';

import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogDescription,
  DialogTitle,
  DialogTrigger,
} from '@/components/ui/dialog';
import type { toggleVariants } from '@/components/ui/toggle';

import { ToolbarButton } from '../toolbar-button';
import { ImageEditBlock } from './image-edit-block';

interface ImageEditDialogProps extends VariantProps<typeof toggleVariants> {
  editor: Editor;
}

const ImageEditDialog = ({ editor, size, variant }: ImageEditDialogProps) => {
  const [open, setOpen] = useState(false);

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger asChild>
        <ToolbarButton
          isActive={editor.isActive('image')}
          tooltip="Image"
          aria-label="Image"
          size={size}
          variant={variant}
        >
          <ImageIcon className="size-4" />
        </ToolbarButton>
      </DialogTrigger>
      <DialogContent className="sm:max-w-lg">
        <DialogHeader>
          <DialogTitle>Select image</DialogTitle>
          <DialogDescription className="sr-only">
            Upload an image from your computer
          </DialogDescription>
        </DialogHeader>
        <ImageEditBlock editor={editor} close={() => setOpen(false)} />
      </DialogContent>
    </Dialog>
  );
};

export { ImageEditDialog };
</file>

<file path="src/components/rich-editor/components/link/link-edit-block.tsx">
import * as React from 'react';

import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';

import { cn } from '@/lib/utils';

export interface LinkEditorProps extends React.HTMLAttributes<HTMLDivElement> {
  defaultUrl?: string;
  defaultText?: string;
  defaultIsNewTab?: boolean;
  onSave: (url: string, text?: string, isNewTab?: boolean) => void;
}

export const LinkEditBlock = React.forwardRef<HTMLDivElement, LinkEditorProps>(
  ({ onSave, defaultIsNewTab, defaultUrl, defaultText, className }, ref) => {
    const formRef = React.useRef<HTMLDivElement>(null);
    const [url, setUrl] = React.useState(defaultUrl || '');
    const [text, setText] = React.useState(defaultText || '');
    const [isNewTab, setIsNewTab] = React.useState(defaultIsNewTab || false);

    const handleSave = React.useCallback(
      (e: React.FormEvent) => {
        e.preventDefault();
        if (formRef.current) {
          const isValid = Array.from(formRef.current.querySelectorAll('input')).every((input) =>
            input.checkValidity(),
          );

          if (isValid) {
            onSave(url, text, isNewTab);
          } else {
            formRef.current.querySelectorAll('input').forEach((input) => {
              if (!input.checkValidity()) {
                input.reportValidity();
              }
            });
          }
        }
      },
      [onSave, url, text, isNewTab],
    );

    React.useImperativeHandle(ref, () => formRef.current as HTMLDivElement);

    return (
      <div ref={formRef}>
        <div className={cn('space-y-4', className)}>
          <div className="space-y-1">
            <Label>URL</Label>
            <Input
              type="url"
              required
              placeholder="Enter URL"
              value={url}
              onChange={(e) => setUrl(e.target.value)}
            />
          </div>

          <div className="space-y-1">
            <Label>Display Text (optional)</Label>
            <Input
              type="text"
              placeholder="Enter display text"
              value={text}
              onChange={(e) => setText(e.target.value)}
            />
          </div>

          <div className="flex items-center space-x-2">
            <Label>Open in New Tab</Label>
            <Switch checked={isNewTab} onCheckedChange={setIsNewTab} />
          </div>

          <div className="flex justify-end space-x-2">
            <Button type="button" onClick={handleSave}>
              Save
            </Button>
          </div>
        </div>
      </div>
    );
  },
);

LinkEditBlock.displayName = 'LinkEditBlock';

export default LinkEditBlock;
</file>

<file path="src/components/rich-editor/components/link/link-edit-popover.tsx">
import { Link2Icon } from '@radix-ui/react-icons';
import type { Editor } from '@tiptap/react';
import type { VariantProps } from 'class-variance-authority';
import * as React from 'react';

import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import type { toggleVariants } from '@/components/ui/toggle';

import { ToolbarButton } from '../toolbar-button';
import { LinkEditBlock } from './link-edit-block';

interface LinkEditPopoverProps extends VariantProps<typeof toggleVariants> {
  editor: Editor;
}

const LinkEditPopover = ({ editor, size, variant }: LinkEditPopoverProps) => {
  const [open, setOpen] = React.useState(false);

  const { from, to } = editor.state.selection;
  const text = editor.state.doc.textBetween(from, to, ' ');

  const onSetLink = React.useCallback(
    (url: string, text?: string, openInNewTab?: boolean) => {
      editor
        .chain()
        .focus()
        .extendMarkRange('link')
        .insertContent({
          type: 'text',
          text: text || url,
          marks: [
            {
              type: 'link',
              attrs: {
                href: url,
                target: openInNewTab ? '_blank' : '',
              },
            },
          ],
        })
        .setLink({ href: url })
        .run();

      editor.commands.enter();
    },
    [editor],
  );

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <ToolbarButton
          isActive={editor.isActive('link')}
          tooltip="Link"
          aria-label="Insert link"
          disabled={editor.isActive('codeBlock')}
          size={size}
          variant={variant}
        >
          <Link2Icon className="size-5" />
        </ToolbarButton>
      </PopoverTrigger>
      <PopoverContent className="w-full min-w-80" align="end" side="bottom">
        <LinkEditBlock onSave={onSetLink} defaultText={text} />
      </PopoverContent>
    </Popover>
  );
};

export { LinkEditPopover };
</file>

<file path="src/components/rich-editor/components/link/link-popover-block.tsx">
import { CopyIcon, ExternalLinkIcon, LinkBreak2Icon } from '@radix-ui/react-icons';
import * as React from 'react';

import { Separator } from '@/components/ui/separator';

import { ToolbarButton } from '../toolbar-button';

interface LinkPopoverBlockProps {
  url: string;
  onClear: () => void;
  onEdit: (e: React.MouseEvent<HTMLButtonElement>) => void;
}

export const LinkPopoverBlock: React.FC<LinkPopoverBlockProps> = ({ url, onClear, onEdit }) => {
  const [copyTitle, setCopyTitle] = React.useState<string>('Copy');

  const handleCopy = React.useCallback(
    (e: React.MouseEvent<HTMLButtonElement>) => {
      e.preventDefault();
      navigator.clipboard
        .writeText(url)
        .then(() => {
          setCopyTitle('Copied!');
          setTimeout(() => setCopyTitle('Copy'), 1000);
        })
        .catch(console.error);
    },
    [url],
  );

  const handleOpenLink = React.useCallback(() => {
    window.open(url, '_blank', 'noopener,noreferrer');
  }, [url]);

  return (
    <div className="flex h-10 overflow-hidden rounded bg-background p-2 shadow-lg">
      <div className="inline-flex items-center gap-1">
        <ToolbarButton tooltip="Edit link" onClick={onEdit} className="w-auto px-2">
          Edit link
        </ToolbarButton>
        <Separator orientation="vertical" />
        <ToolbarButton tooltip="Open link in a new tab" onClick={handleOpenLink}>
          <ExternalLinkIcon className="size-4" />
        </ToolbarButton>
        <Separator orientation="vertical" />
        <ToolbarButton tooltip="Clear link" onClick={onClear}>
          <LinkBreak2Icon className="size-4" />
        </ToolbarButton>
        <Separator orientation="vertical" />
        <ToolbarButton
          tooltip={copyTitle}
          onClick={handleCopy}
          tooltipOptions={{
            onPointerDownOutside: (e) => {
              if (e.target === e.currentTarget) e.preventDefault();
            },
          }}
        >
          <CopyIcon className="size-4" />
        </ToolbarButton>
      </div>
    </div>
  );
};
</file>

<file path="src/components/rich-editor/components/section/five.tsx">
import {
  CaretDownIcon,
  CodeIcon,
  DividerHorizontalIcon,
  PlusIcon,
  QuoteIcon,
} from '@radix-ui/react-icons';
import type { Editor } from '@tiptap/react';
import type { VariantProps } from 'class-variance-authority';
import * as React from 'react';

import type { toggleVariants } from '@/components/ui/toggle';

import type { FormatAction } from '../../types';
import { ImageEditDialog } from '../image/image-edit-dialog';
import { LinkEditPopover } from '../link/link-edit-popover';
import { ToolbarSection } from '../toolbar-section';

type InsertElementAction = 'codeBlock' | 'blockquote' | 'horizontalRule';
interface InsertElement extends FormatAction {
  value: InsertElementAction;
}

const formatActions: InsertElement[] = [
  {
    value: 'codeBlock',
    label: 'Code block',
    icon: <CodeIcon className="size-5" />,
    action: (editor) => editor.chain().focus().toggleCodeBlock().run(),
    isActive: (editor) => editor.isActive('codeBlock'),
    canExecute: (editor) => editor.can().chain().focus().toggleCodeBlock().run(),
    shortcuts: ['mod', 'alt', 'C'],
  },
  {
    value: 'blockquote',
    label: 'Blockquote',
    icon: <QuoteIcon className="size-5" />,
    action: (editor) => editor.chain().focus().toggleBlockquote().run(),
    isActive: (editor) => editor.isActive('blockquote'),
    canExecute: (editor) => editor.can().chain().focus().toggleBlockquote().run(),
    shortcuts: ['mod', 'shift', 'B'],
  },
  {
    value: 'horizontalRule',
    label: 'Divider',
    icon: <DividerHorizontalIcon className="size-5" />,
    action: (editor) => editor.chain().focus().setHorizontalRule().run(),
    isActive: () => false,
    canExecute: (editor) => editor.can().chain().focus().setHorizontalRule().run(),
    shortcuts: ['mod', 'alt', '-'],
  },
];

interface SectionFiveProps extends VariantProps<typeof toggleVariants> {
  editor: Editor;
  activeActions?: InsertElementAction[];
  mainActionCount?: number;
}

export const SectionFive: React.FC<SectionFiveProps> = ({
  editor,
  activeActions = formatActions.map((action) => action.value),
  mainActionCount = 0,
  size,
  variant,
}) => {
  return (
    <>
      <LinkEditPopover editor={editor} size={size} variant={variant} />
      <ImageEditDialog editor={editor} size={size} variant={variant} />
      <ToolbarSection
        editor={editor}
        actions={formatActions}
        activeActions={activeActions}
        mainActionCount={mainActionCount}
        dropdownIcon={
          <>
            <PlusIcon className="size-5" />
            <CaretDownIcon className="size-5" />
          </>
        }
        dropdownClassName="w-12 !gap-0"
        dropdownTooltip="Insert elements"
        size={size}
        variant={variant}
      />
    </>
  );
};

SectionFive.displayName = 'SectionFive';

export default SectionFive;
</file>

<file path="src/components/rich-editor/components/section/four.tsx">
import { CaretDownIcon, ListBulletIcon } from '@radix-ui/react-icons';
import type { Editor } from '@tiptap/react';
import type { VariantProps } from 'class-variance-authority';
import * as React from 'react';

import type { toggleVariants } from '@/components/ui/toggle';

import type { FormatAction } from '../../types';
import { ToolbarSection } from '../toolbar-section';

type ListItemAction = 'orderedList' | 'bulletList';
interface ListItem extends FormatAction {
  value: ListItemAction;
}

const formatActions: ListItem[] = [
  {
    value: 'orderedList',
    label: 'Numbered list',
    icon: (
      <svg
        xmlns="http://www.w3.org/2000/svg"
        height="20px"
        viewBox="0 -960 960 960"
        width="20px"
        fill="currentColor"
      >
        <path d="M144-144v-48h96v-24h-48v-48h48v-24h-96v-48h120q10.2 0 17.1 6.9 6.9 6.9 6.9 17.1v48q0 10.2-6.9 17.1-6.9 6.9-17.1 6.9 10.2 0 17.1 6.9 6.9 6.9 6.9 17.1v48q0 10.2-6.9 17.1-6.9 6.9-17.1 6.9H144Zm0-240v-96q0-10.2 6.9-17.1 6.9-6.9 17.1-6.9h72v-24h-96v-48h120q10.2 0 17.1 6.9 6.9 6.9 6.9 17.1v72q0 10.2-6.9 17.1-6.9 6.9-17.1 6.9h-72v24h96v48H144Zm48-240v-144h-48v-48h96v192h-48Zm168 384v-72h456v72H360Zm0-204v-72h456v72H360Zm0-204v-72h456v72H360Z" />
      </svg>
    ),
    isActive: (editor) => editor.isActive('orderedList'),
    action: (editor) => editor.chain().focus().toggleOrderedList().run(),
    canExecute: (editor) => editor.can().chain().focus().toggleOrderedList().run(),
    shortcuts: ['mod', 'shift', '7'],
  },
  {
    value: 'bulletList',
    label: 'Bullet list',
    icon: <ListBulletIcon className="size-5" />,
    isActive: (editor) => editor.isActive('bulletList'),
    action: (editor) => editor.chain().focus().toggleBulletList().run(),
    canExecute: (editor) => editor.can().chain().focus().toggleBulletList().run(),
    shortcuts: ['mod', 'shift', '8'],
  },
];

interface SectionFourProps extends VariantProps<typeof toggleVariants> {
  editor: Editor;
  activeActions?: ListItemAction[];
  mainActionCount?: number;
}

export const SectionFour: React.FC<SectionFourProps> = ({
  editor,
  activeActions = formatActions.map((action) => action.value),
  mainActionCount = 0,
  size,
  variant,
}) => {
  return (
    <ToolbarSection
      editor={editor}
      actions={formatActions}
      activeActions={activeActions}
      mainActionCount={mainActionCount}
      dropdownIcon={
        <>
          <ListBulletIcon className="size-5" />
          <CaretDownIcon className="size-5" />
        </>
      }
      dropdownClassName="w-14"
      dropdownTooltip="Lists"
      size={size}
      variant={variant}
    />
  );
};

SectionFour.displayName = 'SectionFour';

export default SectionFour;
</file>

<file path="src/components/rich-editor/components/section/one.tsx">
import { CaretDownIcon, LetterCaseCapitalizeIcon } from '@radix-ui/react-icons';
import type { Level } from '@tiptap/extension-heading';
import type { Editor } from '@tiptap/react';
import type { VariantProps } from 'class-variance-authority';
import * as React from 'react';

import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import type { toggleVariants } from '@/components/ui/toggle';

import { cn } from '@/lib/utils';

import type { FormatAction } from '../../types';
import { ShortcutKey } from '../shortcut-key';
import { ToolbarButton } from '../toolbar-button';

interface TextStyle
  extends Omit<FormatAction, 'value' | 'icon' | 'action' | 'isActive' | 'canExecute'> {
  element: keyof React.JSX.IntrinsicElements;
  level?: Level;
  className: string;
}

const formatActions: TextStyle[] = [
  {
    label: 'Normal Text',
    element: 'span',
    className: 'grow',
    shortcuts: ['mod', 'alt', '0'],
  },
  {
    label: 'Heading 1',
    element: 'h1',
    level: 1,
    className: 'm-0 grow text-3xl font-extrabold',
    shortcuts: ['mod', 'alt', '1'],
  },
  {
    label: 'Heading 2',
    element: 'h2',
    level: 2,
    className: 'm-0 grow text-xl font-bold',
    shortcuts: ['mod', 'alt', '2'],
  },
  {
    label: 'Heading 3',
    element: 'h3',
    level: 3,
    className: 'm-0 grow text-lg font-semibold',
    shortcuts: ['mod', 'alt', '3'],
  },
  {
    label: 'Heading 4',
    element: 'h4',
    level: 4,
    className: 'm-0 grow text-base font-semibold',
    shortcuts: ['mod', 'alt', '4'],
  },
  {
    label: 'Heading 5',
    element: 'h5',
    level: 5,
    className: 'm-0 grow text-sm font-normal',
    shortcuts: ['mod', 'alt', '5'],
  },
  {
    label: 'Heading 6',
    element: 'h6',
    level: 6,
    className: 'm-0 grow text-sm font-normal',
    shortcuts: ['mod', 'alt', '6'],
  },
];

interface SectionOneProps extends VariantProps<typeof toggleVariants> {
  editor: Editor;
  activeLevels?: Level[];
}

export const SectionOne: React.FC<SectionOneProps> = React.memo(
  ({ editor, activeLevels = [1, 2, 3, 4, 5, 6], size, variant }) => {
    const filteredActions = React.useMemo(
      () => formatActions.filter((action) => !action.level || activeLevels.includes(action.level)),
      [activeLevels],
    );

    const handleStyleChange = React.useCallback(
      (level?: Level) => {
        if (level) {
          editor.chain().focus().toggleHeading({ level }).run();
        } else {
          editor.chain().focus().setParagraph().run();
        }
      },
      [editor],
    );

    const renderMenuItem = React.useCallback(
      ({ label, element: Element, level, className, shortcuts }: TextStyle) => (
        <DropdownMenuItem
          key={label}
          onClick={() => handleStyleChange(level)}
          className={cn('flex flex-row items-center justify-between gap-4', {
            'bg-accent': level
              ? editor.isActive('heading', { level })
              : editor.isActive('paragraph'),
          })}
          aria-label={label}
        >
          <Element className={className}>{label}</Element>
          <ShortcutKey keys={shortcuts} />
        </DropdownMenuItem>
      ),
      [editor, handleStyleChange],
    );

    return (
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <ToolbarButton
            isActive={editor.isActive('heading')}
            tooltip="Text styles"
            aria-label="Text styles"
            pressed={editor.isActive('heading')}
            className="w-15"
            disabled={editor.isActive('codeBlock')}
            size={size}
            variant={variant}
          >
            <LetterCaseCapitalizeIcon className="size-5" />
            <CaretDownIcon className="size-5" />
          </ToolbarButton>
        </DropdownMenuTrigger>
        <DropdownMenuContent
          onCloseAutoFocus={(e) => e.preventDefault()}
          align="start"
          className="w-full"
        >
          {filteredActions.map(renderMenuItem)}
        </DropdownMenuContent>
      </DropdownMenu>
    );
  },
);

SectionOne.displayName = 'SectionOne';

export default SectionOne;
</file>

<file path="src/components/rich-editor/components/section/three.tsx">
import { CaretDownIcon, CheckIcon } from '@radix-ui/react-icons';
import type { Editor } from '@tiptap/react';
import type { VariantProps } from 'class-variance-authority';
import * as React from 'react';

import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover';
import type { toggleVariants } from '@/components/ui/toggle';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';

import { useTheme } from '../../hooks/use-theme';
import { ToolbarButton } from '../toolbar-button';

interface ColorItem {
  cssVar: string;
  label: string;
  darkLabel?: string;
}

interface ColorPalette {
  label: string;
  colors: ColorItem[];
  inverse: string;
}

const COLORS: ColorPalette[] = [
  {
    label: 'Palette 1',
    inverse: 'hsl(var(--background))',
    colors: [
      { cssVar: 'hsl(var(--foreground))', label: 'Default' },
      { cssVar: 'var(--mt-accent-bold-blue)', label: 'Bold blue' },
      { cssVar: 'var(--mt-accent-bold-teal)', label: 'Bold teal' },
      { cssVar: 'var(--mt-accent-bold-green)', label: 'Bold green' },
      { cssVar: 'var(--mt-accent-bold-orange)', label: 'Bold orange' },
      { cssVar: 'var(--mt-accent-bold-red)', label: 'Bold red' },
      { cssVar: 'var(--mt-accent-bold-purple)', label: 'Bold purple' },
    ],
  },
  {
    label: 'Palette 2',
    inverse: 'hsl(var(--background))',
    colors: [
      { cssVar: 'var(--mt-accent-gray)', label: 'Gray' },
      { cssVar: 'var(--mt-accent-blue)', label: 'Blue' },
      { cssVar: 'var(--mt-accent-teal)', label: 'Teal' },
      { cssVar: 'var(--mt-accent-green)', label: 'Green' },
      { cssVar: 'var(--mt-accent-orange)', label: 'Orange' },
      { cssVar: 'var(--mt-accent-red)', label: 'Red' },
      { cssVar: 'var(--mt-accent-purple)', label: 'Purple' },
    ],
  },
  {
    label: 'Palette 3',
    inverse: 'hsl(var(--foreground))',
    colors: [
      { cssVar: 'hsl(var(--background))', label: 'White', darkLabel: 'Black' },
      { cssVar: 'var(--mt-accent-blue-subtler)', label: 'Blue subtle' },
      { cssVar: 'var(--mt-accent-teal-subtler)', label: 'Teal subtle' },
      { cssVar: 'var(--mt-accent-green-subtler)', label: 'Green subtle' },
      { cssVar: 'var(--mt-accent-yellow-subtler)', label: 'Yellow subtle' },
      { cssVar: 'var(--mt-accent-red-subtler)', label: 'Red subtle' },
      { cssVar: 'var(--mt-accent-purple-subtler)', label: 'Purple subtle' },
    ],
  },
];

const MemoizedColorButton = React.memo<{
  color: ColorItem;
  isSelected: boolean;
  inverse: string;
  onClick: (value: string) => void;
}>(({ color, isSelected, inverse, onClick }) => {
  const isDarkMode = useTheme();
  const label = isDarkMode && color.darkLabel ? color.darkLabel : color.label;

  return (
    <Tooltip>
      <TooltipTrigger asChild>
        <ToggleGroupItem
          tabIndex={0}
          className="relative size-7 rounded-md p-0"
          value={color.cssVar}
          aria-label={label}
          style={{ backgroundColor: color.cssVar }}
          onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
            e.preventDefault();
            onClick(color.cssVar);
          }}
        >
          {isSelected && (
            <CheckIcon className="absolute inset-0 m-auto size-6" style={{ color: inverse }} />
          )}
        </ToggleGroupItem>
      </TooltipTrigger>
      <TooltipContent side="bottom">
        <p>{label}</p>
      </TooltipContent>
    </Tooltip>
  );
});

MemoizedColorButton.displayName = 'MemoizedColorButton';

const MemoizedColorPicker = React.memo<{
  palette: ColorPalette;
  selectedColor: string;
  inverse: string;
  onColorChange: (value: string) => void;
}>(({ palette, selectedColor, inverse, onColorChange }) => (
  <ToggleGroup
    type="single"
    value={selectedColor}
    onValueChange={(value: string) => {
      if (value) onColorChange(value);
    }}
    className="gap-1.5"
  >
    {palette.colors.map((color, index) => (
      <MemoizedColorButton
        key={index}
        inverse={inverse}
        color={color}
        isSelected={selectedColor === color.cssVar}
        onClick={onColorChange}
      />
    ))}
  </ToggleGroup>
));

MemoizedColorPicker.displayName = 'MemoizedColorPicker';

interface SectionThreeProps extends VariantProps<typeof toggleVariants> {
  editor: Editor;
}

export const SectionThree: React.FC<SectionThreeProps> = ({ editor, size, variant }) => {
  const color = editor.getAttributes('textStyle')?.color || 'hsl(var(--foreground))';
  const [selectedColor, setSelectedColor] = React.useState(color);

  const handleColorChange = React.useCallback(
    (value: string) => {
      setSelectedColor(value);
      editor.chain().setColor(value).run();
    },
    [editor],
  );

  React.useEffect(() => {
    setSelectedColor(color);
  }, [color]);

  return (
    <Popover>
      <PopoverTrigger asChild>
        <ToolbarButton
          tooltip="Text color"
          aria-label="Text color"
          className="w-14"
          size={size}
          variant={variant}
        >
          <svg
            xmlns="http://www.w3.org/2000/svg"
            width="24"
            height="24"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            strokeWidth="2"
            strokeLinecap="round"
            strokeLinejoin="round"
            className="size-5"
            style={{ color: selectedColor }}
          >
            <path d="M4 20h16" />
            <path d="m6 16 6-12 6 12" />
            <path d="M8 12h8" />
          </svg>
          <CaretDownIcon className="size-5" />
        </ToolbarButton>
      </PopoverTrigger>
      <PopoverContent align="start" className="w-full">
        <div className="space-y-1.5">
          {COLORS.map((palette, index) => (
            <MemoizedColorPicker
              key={index}
              palette={palette}
              inverse={palette.inverse}
              selectedColor={selectedColor}
              onColorChange={handleColorChange}
            />
          ))}
        </div>
      </PopoverContent>
    </Popover>
  );
};

SectionThree.displayName = 'SectionThree';

export default SectionThree;
</file>

<file path="src/components/rich-editor/components/section/two.tsx">
import {
  CodeIcon,
  DotsHorizontalIcon,
  FontBoldIcon,
  FontItalicIcon,
  StrikethroughIcon,
  TextNoneIcon,
  UnderlineIcon,
} from '@radix-ui/react-icons';
import type { Editor } from '@tiptap/react';
import type { VariantProps } from 'class-variance-authority';
import * as React from 'react';

import type { toggleVariants } from '@/components/ui/toggle';

import type { FormatAction } from '../../types';
import { ToolbarSection } from '../toolbar-section';

type TextStyleAction =
  | 'bold'
  | 'italic'
  | 'underline'
  | 'strikethrough'
  | 'code'
  | 'clearFormatting';

interface TextStyle extends FormatAction {
  value: TextStyleAction;
}

const formatActions: TextStyle[] = [
  {
    value: 'bold',
    label: 'Bold',
    icon: <FontBoldIcon className="size-5" />,
    action: (editor) => editor.chain().focus().toggleBold().run(),
    isActive: (editor) => editor.isActive('bold'),
    canExecute: (editor) =>
      editor.can().chain().focus().toggleBold().run() && !editor.isActive('codeBlock'),
    shortcuts: ['mod', 'B'],
  },
  {
    value: 'italic',
    label: 'Italic',
    icon: <FontItalicIcon className="size-5" />,
    action: (editor) => editor.chain().focus().toggleItalic().run(),
    isActive: (editor) => editor.isActive('italic'),
    canExecute: (editor) =>
      editor.can().chain().focus().toggleItalic().run() && !editor.isActive('codeBlock'),
    shortcuts: ['mod', 'I'],
  },
  {
    value: 'underline',
    label: 'Underline',
    icon: <UnderlineIcon className="size-5" />,
    action: (editor) => editor.chain().focus().toggleUnderline().run(),
    isActive: (editor) => editor.isActive('underline'),
    canExecute: (editor) =>
      editor.can().chain().focus().toggleUnderline().run() && !editor.isActive('codeBlock'),
    shortcuts: ['mod', 'U'],
  },
  {
    value: 'strikethrough',
    label: 'Strikethrough',
    icon: <StrikethroughIcon className="size-5" />,
    action: (editor) => editor.chain().focus().toggleStrike().run(),
    isActive: (editor) => editor.isActive('strike'),
    canExecute: (editor) =>
      editor.can().chain().focus().toggleStrike().run() && !editor.isActive('codeBlock'),
    shortcuts: ['mod', 'shift', 'S'],
  },
  {
    value: 'code',
    label: 'Code',
    icon: <CodeIcon className="size-5" />,
    action: (editor) => editor.chain().focus().toggleCode().run(),
    isActive: (editor) => editor.isActive('code'),
    canExecute: (editor) =>
      editor.can().chain().focus().toggleCode().run() && !editor.isActive('codeBlock'),
    shortcuts: ['mod', 'E'],
  },
  {
    value: 'clearFormatting',
    label: 'Clear formatting',
    icon: <TextNoneIcon className="size-5" />,
    action: (editor) => editor.chain().focus().unsetAllMarks().run(),
    isActive: () => false,
    canExecute: (editor) =>
      editor.can().chain().focus().unsetAllMarks().run() && !editor.isActive('codeBlock'),
    shortcuts: ['mod', '\\'],
  },
];

interface SectionTwoProps extends VariantProps<typeof toggleVariants> {
  editor: Editor;
  activeActions?: TextStyleAction[];
  mainActionCount?: number;
}

export const SectionTwo: React.FC<SectionTwoProps> = ({
  editor,
  activeActions = formatActions.map((action) => action.value),
  mainActionCount = 2,
  size,
  variant,
}) => {
  return (
    <ToolbarSection
      editor={editor}
      actions={formatActions}
      activeActions={activeActions}
      mainActionCount={mainActionCount}
      dropdownIcon={<DotsHorizontalIcon className="size-5" />}
      dropdownTooltip="More formatting"
      dropdownClassName="w-8"
      size={size}
      variant={variant}
    />
  );
};

SectionTwo.displayName = 'SectionTwo';

export default SectionTwo;
</file>

<file path="src/components/rich-editor/components/measured-container.tsx">
import * as React from 'react';

import { useContainerSize } from '../hooks/use-container-size';

interface MeasuredContainerProps<T extends React.ElementType> {
  as: T;
  name: string;
  children?: React.ReactNode;
}

export const MeasuredContainer = React.forwardRef(
  <T extends React.ElementType>(
    {
      as: Component,
      name,
      children,
      style = {},
      ...props
    }: MeasuredContainerProps<T> & React.ComponentProps<T>,
    ref: React.Ref<HTMLElement>,
  ) => {
    const innerRef = React.useRef<HTMLElement>(null);
    const rect = useContainerSize(innerRef.current);

    React.useImperativeHandle(ref, () => innerRef.current as HTMLElement);

    const customStyle = {
      [`--${name}-width`]: `${rect.width}px`,
      [`--${name}-height`]: `${rect.height}px`,
    };

    return (
      <Component {...props} ref={innerRef} style={{ ...customStyle, ...style }}>
        {children}
      </Component>
    );
  },
);

MeasuredContainer.displayName = 'MeasuredContainer';
</file>

<file path="src/components/rich-editor/components/shortcut-key.tsx">
import * as React from 'react';

import { cn } from '@/lib/utils';

import { getShortcutKey } from '../utils';

export interface ShortcutKeyProps extends React.HTMLAttributes<HTMLSpanElement> {
  keys: string[];
}

export const ShortcutKey = React.forwardRef<HTMLSpanElement, ShortcutKeyProps>(
  ({ className, keys, ...props }, ref) => {
    const modifiedKeys = keys.map((key) => getShortcutKey(key));
    const ariaLabel = modifiedKeys.map((shortcut) => shortcut.readable).join(' + ');

    return (
      <span
        aria-label={ariaLabel}
        className={cn('inline-flex items-center gap-0.5', className)}
        {...props}
        ref={ref}
      >
        {modifiedKeys.map((shortcut) => (
          <kbd
            key={shortcut.symbol}
            className={cn(
              'inline-block min-w-2.5 text-center align-baseline font-sans text-xs font-medium capitalize text-[rgb(156,157,160)]',

              className,
            )}
            {...props}
            ref={ref}
          >
            {shortcut.symbol}
          </kbd>
        ))}
      </span>
    );
  },
);

ShortcutKey.displayName = 'ShortcutKey';
</file>

<file path="src/components/rich-editor/components/spinner.tsx">
import * as React from 'react';

import { cn } from '@/lib/utils';

type SpinnerProps = React.SVGProps<SVGSVGElement>;

const SpinnerComponent = React.forwardRef<SVGSVGElement, SpinnerProps>(function Spinner(
  { className, ...props },
  ref,
) {
  return (
    <svg
      ref={ref}
      xmlns="http://www.w3.org/2000/svg"
      fill="none"
      viewBox="0 0 24 24"
      className={cn('animate-spin', className)}
      {...props}
    >
      <circle
        className="opacity-25"
        cx="12"
        cy="12"
        r="10"
        stroke="currentColor"
        strokeWidth="4"
      ></circle>
      <path
        className="opacity-75"
        fill="currentColor"
        d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
      ></path>
    </svg>
  );
});

SpinnerComponent.displayName = 'Spinner';

export const Spinner = React.memo(SpinnerComponent);
</file>

<file path="src/components/rich-editor/components/toolbar-button.tsx">
import type { TooltipContentProps } from '@radix-ui/react-tooltip';
import * as React from 'react';

import { Toggle } from '@/components/ui/toggle';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';

import { cn } from '@/lib/utils';

interface ToolbarButtonProps extends React.ComponentPropsWithoutRef<typeof Toggle> {
  isActive?: boolean;
  tooltip?: string;
  tooltipOptions?: TooltipContentProps;
}

export const ToolbarButton = React.forwardRef<HTMLButtonElement, ToolbarButtonProps>(
  ({ isActive, children, tooltip, className, tooltipOptions, ...props }, ref) => {
    const toggleButton = (
      <Toggle
        size="sm"
        ref={ref}
        className={cn('size-8 p-0 gap-1 min-w-8', { 'bg-accent': isActive }, className)}
        {...props}
      >
        {children}
      </Toggle>
    );

    if (!tooltip) {
      return toggleButton;
    }

    return (
      <Tooltip>
        <TooltipTrigger asChild>{toggleButton}</TooltipTrigger>
        <TooltipContent {...tooltipOptions}>
          <div className="flex flex-col items-center text-center">{tooltip}</div>
        </TooltipContent>
      </Tooltip>
    );
  },
);

ToolbarButton.displayName = 'ToolbarButton';

export default ToolbarButton;
</file>

<file path="src/components/rich-editor/components/toolbar-section.tsx">
import { CaretDownIcon } from '@radix-ui/react-icons';
import type { Editor } from '@tiptap/react';
import type { VariantProps } from 'class-variance-authority';
import * as React from 'react';

import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import type { toggleVariants } from '@/components/ui/toggle';

import { cn } from '@/lib/utils';

import type { FormatAction } from '../types';
import { getShortcutKey } from '../utils';
import { ShortcutKey } from './shortcut-key';
import { ToolbarButton } from './toolbar-button';

interface ToolbarSectionProps extends VariantProps<typeof toggleVariants> {
  editor: Editor;
  actions: FormatAction[];
  activeActions?: string[];
  mainActionCount?: number;
  dropdownIcon?: React.ReactNode;
  dropdownTooltip?: string;
  dropdownClassName?: string;
}

export const ToolbarSection: React.FC<ToolbarSectionProps> = ({
  editor,
  actions,
  activeActions = actions.map((action) => action.value),
  mainActionCount = 0,
  dropdownIcon,
  dropdownTooltip = 'More options',
  dropdownClassName = 'w-12',
  size,
  variant,
}) => {
  const { mainActions, dropdownActions } = React.useMemo(() => {
    const sortedActions = actions
      .filter((action) => activeActions.includes(action.value))
      .sort((a, b) => activeActions.indexOf(a.value) - activeActions.indexOf(b.value));

    return {
      mainActions: sortedActions.slice(0, mainActionCount),
      dropdownActions: sortedActions.slice(mainActionCount),
    };
  }, [actions, activeActions, mainActionCount]);

  const renderToolbarButton = React.useCallback(
    (action: FormatAction) => (
      <ToolbarButton
        key={action.label}
        onClick={() => action.action(editor)}
        disabled={!action.canExecute(editor)}
        isActive={action.isActive(editor)}
        tooltip={`${action.label} ${action.shortcuts.map((s) => getShortcutKey(s).symbol).join(' ')}`}
        aria-label={action.label}
        size={size}
        variant={variant}
      >
        {action.icon}
      </ToolbarButton>
    ),
    [editor, size, variant],
  );

  const renderDropdownMenuItem = React.useCallback(
    (action: FormatAction) => (
      <DropdownMenuItem
        key={action.label}
        onClick={() => action.action(editor)}
        disabled={!action.canExecute(editor)}
        className={cn('flex flex-row items-center justify-between gap-4', {
          'bg-accent': action.isActive(editor),
        })}
        aria-label={action.label}
      >
        <span className="grow">{action.label}</span>
        <ShortcutKey keys={action.shortcuts} />
      </DropdownMenuItem>
    ),
    [editor],
  );

  const isDropdownActive = React.useMemo(
    () => dropdownActions.some((action) => action.isActive(editor)),
    [dropdownActions, editor],
  );

  return (
    <>
      {mainActions.map(renderToolbarButton)}
      {dropdownActions.length > 0 && (
        <DropdownMenu>
          <DropdownMenuTrigger asChild>
            <ToolbarButton
              isActive={isDropdownActive}
              tooltip={dropdownTooltip}
              aria-label={dropdownTooltip}
              className={cn(dropdownClassName)}
              size={size}
              variant={variant}
            >
              {dropdownIcon || <CaretDownIcon className="size-5" />}
            </ToolbarButton>
          </DropdownMenuTrigger>
          <DropdownMenuContent
            onCloseAutoFocus={(e) => e.preventDefault()}
            align="start"
            className="w-full"
          >
            {dropdownActions.map(renderDropdownMenuItem)}
          </DropdownMenuContent>
        </DropdownMenu>
      )}
    </>
  );
};

export default ToolbarSection;
</file>

<file path="src/components/rich-editor/extensions/code-block-lowlight/code-block-lowlight.ts">
import { CodeBlockLowlight as TiptapCodeBlockLowlight } from '@tiptap/extension-code-block-lowlight';
import { common, createLowlight } from 'lowlight';

export const CodeBlockLowlight = TiptapCodeBlockLowlight.extend({
  addOptions() {
    return {
      ...this.parent?.(),
      lowlight: createLowlight(common),
      defaultLanguage: null,
      HTMLAttributes: {
        class: 'block-node',
      },
    };
  },
});

export default CodeBlockLowlight;
</file>

<file path="src/components/rich-editor/extensions/code-block-lowlight/index.ts">
export * from './code-block-lowlight';
</file>

<file path="src/components/rich-editor/extensions/color/color.ts">
import { Color as TiptapColor } from '@tiptap/extension-color';
import { Plugin } from '@tiptap/pm/state';

export const Color = TiptapColor.extend({
  addProseMirrorPlugins() {
    return [
      ...(this.parent?.() || []),
      new Plugin({
        props: {
          handleKeyDown: (_, event) => {
            if (event.key === 'Enter') {
              this.editor.commands.unsetColor();
            }

            return false;
          },
        },
      }),
    ];
  },
});
</file>

<file path="src/components/rich-editor/extensions/color/index.ts">
export * from './color';
</file>

<file path="src/components/rich-editor/extensions/file-handler/index.ts">
import { Plugin, PluginKey } from '@tiptap/pm/state';
import { type Editor, Extension } from '@tiptap/react';

import type { FileError, FileValidationOptions } from '../../utils';
import { filterFiles } from '../../utils';

type FileHandlePluginOptions = {
  key?: PluginKey;
  editor: Editor;
  onPaste?: (editor: Editor, files: File[], pasteContent?: string) => void;
  onDrop?: (editor: Editor, files: File[], pos: number) => void;
  onValidationError?: (errors: FileError[]) => void;
} & FileValidationOptions;

const FileHandlePlugin = (options: FileHandlePluginOptions) => {
  const { key, editor, onPaste, onDrop, onValidationError, allowedMimeTypes, maxFileSize } =
    options;

  return new Plugin({
    key: key || new PluginKey('fileHandler'),

    props: {
      handleDrop(view, event) {
        event.preventDefault();
        event.stopPropagation();

        const { dataTransfer } = event;

        if (!dataTransfer?.files.length) {
          return;
        }

        const pos = view.posAtCoords({
          left: event.clientX,
          top: event.clientY,
        });

        const [validFiles, errors] = filterFiles(Array.from(dataTransfer.files), {
          allowedMimeTypes,
          maxFileSize,
          allowBase64: options.allowBase64,
        });

        if (errors.length > 0 && onValidationError) {
          onValidationError(errors);
        }

        if (validFiles.length > 0 && onDrop) {
          onDrop(editor, validFiles, pos?.pos ?? 0);
        }
      },

      handlePaste(_, event) {
        event.preventDefault();
        event.stopPropagation();

        const { clipboardData } = event;

        if (!clipboardData?.files.length) {
          return;
        }

        const [validFiles, errors] = filterFiles(Array.from(clipboardData.files), {
          allowedMimeTypes,
          maxFileSize,
          allowBase64: options.allowBase64,
        });
        const html = clipboardData.getData('text/html');

        if (errors.length > 0 && onValidationError) {
          onValidationError(errors);
        }

        if (validFiles.length > 0 && onPaste) {
          onPaste(editor, validFiles, html);
        }
      },
    },
  });
};

export const FileHandler = Extension.create<Omit<FileHandlePluginOptions, 'key' | 'editor'>>({
  name: 'fileHandler',

  addOptions() {
    return {
      allowBase64: false,
      allowedMimeTypes: [],
      maxFileSize: 0,
    };
  },

  addProseMirrorPlugins() {
    return [
      FileHandlePlugin({
        key: new PluginKey(this.name),
        editor: this.editor,
        ...this.options,
      }),
    ];
  },
});
</file>

<file path="src/components/rich-editor/extensions/horizontal-rule/horizontal-rule.ts">
/*
 * Wrap the horizontal rule in a div element.
 * Also add a keyboard shortcut to insert a horizontal rule.
 */
import { HorizontalRule as TiptapHorizontalRule } from '@tiptap/extension-horizontal-rule';

export const HorizontalRule = TiptapHorizontalRule.extend({
  addKeyboardShortcuts() {
    return {
      'Mod-Alt--': () =>
        this.editor.commands.insertContent({
          type: this.name,
        }),
    };
  },
});

export default HorizontalRule;
</file>

<file path="src/components/rich-editor/extensions/horizontal-rule/index.ts">
export * from './horizontal-rule';
</file>

<file path="src/components/rich-editor/extensions/image/index.ts">

</file>

<file path="src/components/rich-editor/extensions/link/index.ts">
export * from './link';
</file>

<file path="src/components/rich-editor/extensions/link/link.ts">
import TiptapLink from '@tiptap/extension-link';
import { Plugin, TextSelection } from '@tiptap/pm/state';
import type { EditorView } from '@tiptap/pm/view';
import { mergeAttributes } from '@tiptap/react';
import { getMarkRange } from '@tiptap/react';

export const Link = TiptapLink.extend({
  /*
   * Determines whether typing next to a link automatically becomes part of the link.
   * In this case, we dont want any characters to be included as part of the link.
   */
  inclusive: false,

  /*
   * Match all <a> elements that have an href attribute, except for:
   * - <a> elements with a data-type attribute set to button
   * - <a> elements with an href attribute that contains 'javascript:'
   */
  parseHTML() {
    return [{ tag: 'a[href]:not([data-type="button"]):not([href *= "javascript:" i])' }];
  },

  renderHTML({ HTMLAttributes }) {
    return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
  },

  addOptions() {
    return {
      ...this.parent?.(),
      openOnClick: false,
      HTMLAttributes: {
        class: 'link',
      },
    };
  },

  addProseMirrorPlugins() {
    const { editor } = this;

    return [
      ...(this.parent?.() || []),
      new Plugin({
        props: {
          handleKeyDown: (_: EditorView, event: KeyboardEvent) => {
            const { selection } = editor.state;

            /*
             * Handles the 'Escape' key press when there's a selection within the link.
             * This will move the cursor to the end of the link.
             */
            if (event.key === 'Escape' && selection.empty !== true) {
              editor.commands.focus(selection.to, { scrollIntoView: false });
            }

            return false;
          },
          handleClick(view, pos) {
            /*
             * Marks the entire link when the user clicks on it.
             */

            const { schema, doc, tr } = view.state;
            const range = getMarkRange(doc.resolve(pos), schema.marks.link);

            if (!range) {
              return;
            }

            const { from, to } = range;
            const start = Math.min(from, to);
            const end = Math.max(from, to);

            if (pos < start || pos > end) {
              return;
            }

            const $start = doc.resolve(start);
            const $end = doc.resolve(end);
            const transaction = tr.setSelection(new TextSelection($start, $end));

            view.dispatch(transaction);
          },
        },
      }),
    ];
  },
});

export default Link;
</file>

<file path="src/components/rich-editor/extensions/reset-marks-on-enter/index.ts">
export * from './reset-marks-on-enter';
</file>

<file path="src/components/rich-editor/extensions/reset-marks-on-enter/reset-marks-on-enter.ts">
import { Extension } from '@tiptap/react';

export const ResetMarksOnEnter = Extension.create({
  name: 'resetMarksOnEnter',

  addKeyboardShortcuts() {
    return {
      Enter: ({ editor }) => {
        if (
          editor.isActive('bold') ||
          editor.isActive('italic') ||
          editor.isActive('strike') ||
          editor.isActive('underline') ||
          editor.isActive('code')
        ) {
          editor.commands.splitBlock({ keepMarks: false });

          return true;
        }

        return false;
      },
    };
  },
});
</file>

<file path="src/components/rich-editor/extensions/selection/index.ts">
export * from './selection';
</file>

<file path="src/components/rich-editor/extensions/selection/selection.ts">
import { Plugin, PluginKey } from '@tiptap/pm/state';
import { Decoration, DecorationSet } from '@tiptap/pm/view';
import { Extension } from '@tiptap/react';

export const Selection = Extension.create({
  name: 'selection',

  addProseMirrorPlugins() {
    const { editor } = this;

    return [
      new Plugin({
        key: new PluginKey('selection'),
        props: {
          decorations(state) {
            if (state.selection.empty) {
              return null;
            }

            if (editor.isFocused === true) {
              return null;
            }

            if (!editor.isEditable) {
              return null;
            }

            return DecorationSet.create(state.doc, [
              Decoration.inline(state.selection.from, state.selection.to, {
                class: 'selection',
              }),
            ]);
          },
        },
      }),
    ];
  },
});

export default Selection;
</file>

<file path="src/components/rich-editor/extensions/unset-all-marks/index.ts">
export * from './unset-all-marks';
</file>

<file path="src/components/rich-editor/extensions/unset-all-marks/unset-all-marks.ts">
import { Extension } from '@tiptap/react';

export const UnsetAllMarks = Extension.create({
  addKeyboardShortcuts() {
    return {
      'Mod-\\': () => this.editor.commands.unsetAllMarks(),
    };
  },
});
</file>

<file path="src/components/rich-editor/extensions/index.ts">
export * from './code-block-lowlight';
export * from './color';
export * from './horizontal-rule';
// export * from './image';
export * from './link';
export * from './selection';
export * from './unset-all-marks';
export * from './reset-marks-on-enter';
export * from './file-handler';
</file>

<file path="src/components/rich-editor/hooks/use-container-size.ts">
import { useState, useEffect, useCallback } from 'react';

const DEFAULT_RECT: DOMRect = {
  top: 0,
  left: 0,
  bottom: 0,
  right: 0,
  x: 0,
  y: 0,
  width: 0,
  height: 0,
  toJSON: () => '{}',
};

export function useContainerSize(element: HTMLElement | null): DOMRect {
  const [size, setSize] = useState<DOMRect>(() => element?.getBoundingClientRect() ?? DEFAULT_RECT);

  const handleResize = useCallback(() => {
    if (!element) return;

    const newRect = element.getBoundingClientRect();

    setSize((prevRect) => {
      if (
        Math.round(prevRect.width) === Math.round(newRect.width) &&
        Math.round(prevRect.height) === Math.round(newRect.height) &&
        Math.round(prevRect.x) === Math.round(newRect.x) &&
        Math.round(prevRect.y) === Math.round(newRect.y)
      ) {
        return prevRect;
      }

      return newRect;
    });
  }, [element]);

  useEffect(() => {
    if (!element) return;

    const resizeObserver = new ResizeObserver(handleResize);
    resizeObserver.observe(element);

    window.addEventListener('click', handleResize);
    window.addEventListener('resize', handleResize);

    return () => {
      resizeObserver.disconnect();
      window.removeEventListener('click', handleResize);
      window.removeEventListener('resize', handleResize);
    };
  }, [element, handleResize]);

  return size;
}
</file>

<file path="src/components/rich-editor/hooks/use-minimal-tiptap.ts">
import { Image } from '@tiptap/extension-image';
import { Placeholder } from '@tiptap/extension-placeholder';
import { TextStyle } from '@tiptap/extension-text-style';
import { Typography } from '@tiptap/extension-typography';
import { Underline } from '@tiptap/extension-underline';
import type { Editor } from '@tiptap/react';
import type { Content, UseEditorOptions } from '@tiptap/react';
import { useEditor } from '@tiptap/react';
import { StarterKit } from '@tiptap/starter-kit';
import * as React from 'react';

import { cn } from '@/lib/utils';

import {
  Link,
  HorizontalRule,
  CodeBlockLowlight,
  Selection,
  Color,
  UnsetAllMarks,
  ResetMarksOnEnter,
} from '../extensions';
import { getOutput } from '../utils';
import { useThrottle } from './use-throttle';

export interface UseMinimalTiptapEditorProps extends UseEditorOptions {
  value?: Content;
  output?: 'html' | 'json' | 'text';
  placeholder?: string;
  editorClassName?: string;
  throttleDelay?: number;
  onUpdate?: (content: Content) => void;
  onBlur?: (content: Content) => void;
}

const createExtensions = (placeholder: string) => [
  StarterKit.configure({
    horizontalRule: false,
    codeBlock: false,
    paragraph: { HTMLAttributes: { class: 'text-node' } },
    heading: { HTMLAttributes: { class: 'heading-node' } },
    blockquote: { HTMLAttributes: { class: 'block-node' } },
    bulletList: { HTMLAttributes: { class: 'list-node' } },
    orderedList: { HTMLAttributes: { class: 'list-node' } },
    code: { HTMLAttributes: { class: 'inline', spellcheck: 'false' } },
    dropcursor: { width: 2, class: 'ProseMirror-dropcursor border' },
  }),
  Link,
  Underline,
  Image.configure(),
  Color,
  TextStyle,
  Selection,
  Typography,
  UnsetAllMarks,
  HorizontalRule,
  ResetMarksOnEnter,
  CodeBlockLowlight,
  Placeholder.configure({ placeholder: () => placeholder }),
];

export const useMinimalTiptapEditor = ({
  value,
  output = 'html',
  placeholder = '',
  editorClassName,
  throttleDelay = 0,
  onUpdate,
  onBlur,
  ...props
}: UseMinimalTiptapEditorProps) => {
  const throttledSetValue = useThrottle((value: Content) => onUpdate?.(value), throttleDelay);

  const handleUpdate = React.useCallback(
    (editor: Editor) => throttledSetValue(getOutput(editor, output)),
    [output, throttledSetValue],
  );

  const handleCreate = React.useCallback(
    (editor: Editor) => {
      if (value && editor.isEmpty) {
        editor.commands.setContent(value);
      }
    },
    [value],
  );

  const handleBlur = React.useCallback(
    (editor: Editor) => onBlur?.(getOutput(editor, output)),
    [output, onBlur],
  );

  const editor = useEditor({
    extensions: createExtensions(placeholder),
    editorProps: {
      attributes: {
        autocomplete: 'off',
        autocorrect: 'off',
        autocapitalize: 'off',
        class: cn('focus:outline-none px-5 py-4 h-full', editorClassName),
      },
    },
    immediatelyRender: false,
    onUpdate: ({ editor }) => handleUpdate(editor),
    onCreate: ({ editor }) => handleCreate(editor),
    onBlur: ({ editor }) => handleBlur(editor),
    ...props,
  });

  return editor;
};

export default useMinimalTiptapEditor;
</file>

<file path="src/components/rich-editor/hooks/use-theme.ts">
import * as React from 'react';

export const useTheme = () => {
  const [isDarkMode, setIsDarkMode] = React.useState(false);

  React.useEffect(() => {
    const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    setIsDarkMode(darkModeMediaQuery.matches);

    const handleChange = (e: MediaQueryListEvent) => {
      const newDarkMode = e.matches;
      setIsDarkMode(newDarkMode);
    };

    darkModeMediaQuery.addEventListener('change', handleChange);

    return () => {
      darkModeMediaQuery.removeEventListener('change', handleChange);
    };
  }, []);

  return isDarkMode;
};

export default useTheme;
</file>

<file path="src/components/rich-editor/hooks/use-throttle.ts">
import { useRef, useCallback } from 'react';

export function useThrottle<T extends (...args: any[]) => void>(
  callback: T,
  delay: number,
): (...args: Parameters<T>) => void {
  const lastRan = useRef(Date.now());
  const timeoutRef = useRef<NodeJS.Timeout | null>(null);

  return useCallback(
    (...args: Parameters<T>) => {
      const handler = () => {
        if (Date.now() - lastRan.current >= delay) {
          callback(...args);
          lastRan.current = Date.now();
        } else {
          if (timeoutRef.current) {
            clearTimeout(timeoutRef.current);
          }
          timeoutRef.current = setTimeout(
            () => {
              callback(...args);
              lastRan.current = Date.now();
            },
            delay - (Date.now() - lastRan.current),
          );
        }
      };

      handler();
    },
    [callback, delay],
  );
}
</file>

<file path="src/components/rich-editor/styles/partials/code.css">
.minimal-tiptap-editor .ProseMirror code.inline {
  @apply rounded border border-[var(--mt-code-color)] bg-[var(--mt-code-background)] px-1 py-0.5 text-sm;
}

.minimal-tiptap-editor .ProseMirror pre {
  @apply relative overflow-auto rounded border font-mono text-sm;
  @apply border-[var(--mt-pre-border)] bg-[var(--mt-pre-background)] text-[var(--mt-pre-color)];
  @apply hyphens-none whitespace-pre text-left;
}

.minimal-tiptap-editor .ProseMirror code {
  @apply break-words leading-[1.7em];
}

.minimal-tiptap-editor .ProseMirror pre code {
  @apply block overflow-x-auto p-3.5;
}

.minimal-tiptap-editor .ProseMirror pre {
  .hljs-keyword,
  .hljs-operator,
  .hljs-function,
  .hljs-built_in,
  .hljs-builtin-name {
    color: var(--hljs-keyword);
  }

  .hljs-attr,
  .hljs-symbol,
  .hljs-property,
  .hljs-attribute,
  .hljs-variable,
  .hljs-template-variable,
  .hljs-params {
    color: var(--hljs-attr);
  }

  .hljs-name,
  .hljs-regexp,
  .hljs-link,
  .hljs-type,
  .hljs-addition {
    color: var(--hljs-name);
  }

  .hljs-string,
  .hljs-bullet {
    color: var(--hljs-string);
  }

  .hljs-title,
  .hljs-subst,
  .hljs-section {
    color: var(--hljs-title);
  }

  .hljs-literal,
  .hljs-type,
  .hljs-deletion {
    color: var(--hljs-literal);
  }

  .hljs-selector-tag,
  .hljs-selector-id,
  .hljs-selector-class {
    color: var(--hljs-selector-tag);
  }

  .hljs-number {
    color: var(--hljs-number);
  }

  .hljs-comment,
  .hljs-meta,
  .hljs-quote {
    color: var(--hljs-comment);
  }

  .hljs-emphasis {
    @apply italic;
  }

  .hljs-strong {
    @apply font-bold;
  }
}
</file>

<file path="src/components/rich-editor/styles/partials/lists.css">
.minimal-tiptap-editor .ProseMirror ol {
  @apply list-decimal;
}

.minimal-tiptap-editor .ProseMirror ol ol {
  list-style: lower-alpha;
}

.minimal-tiptap-editor .ProseMirror ol ol ol {
  list-style: lower-roman;
}

.minimal-tiptap-editor .ProseMirror ul {
  list-style: disc;
}

.minimal-tiptap-editor .ProseMirror ul ul {
  list-style: circle;
}

.minimal-tiptap-editor .ProseMirror ul ul ul {
  list-style: square;
}
</file>

<file path="src/components/rich-editor/styles/partials/placeholder.css">
.minimal-tiptap-editor .ProseMirror > p.is-editor-empty::before {
  content: attr(data-placeholder);
  @apply pointer-events-none float-left h-0 text-[var(--mt-secondary)];
}
</file>

<file path="src/components/rich-editor/styles/partials/typography.css">
.minimal-tiptap-editor .ProseMirror .heading-node {
  @apply relative font-semibold;
}

.minimal-tiptap-editor .ProseMirror .heading-node:first-child {
  @apply mt-0;
}

.minimal-tiptap-editor .ProseMirror h1 {
  @apply mb-4 mt-[46px] text-[1.375rem] leading-7 tracking-[-0.004375rem];
}

.minimal-tiptap-editor .ProseMirror h2 {
  @apply mb-3.5 mt-8 text-[1.1875rem] leading-7 tracking-[0.003125rem];
}

.minimal-tiptap-editor .ProseMirror h3 {
  @apply mb-3 mt-6 text-[1.0625rem] leading-6 tracking-[0.00625rem];
}

.minimal-tiptap-editor .ProseMirror h4 {
  @apply mb-2 mt-4 text-[0.9375rem] leading-6;
}

.minimal-tiptap-editor .ProseMirror h5 {
  @apply mb-2 mt-4 text-sm;
}

.minimal-tiptap-editor .ProseMirror h5 {
  @apply mb-2 mt-4 text-sm;
}

.minimal-tiptap-editor .ProseMirror a.link {
  @apply cursor-pointer text-primary;
}

.minimal-tiptap-editor .ProseMirror a.link:hover {
  @apply underline;
}
</file>

<file path="src/components/rich-editor/styles/partials/zoom.css">
[data-rmiz-ghost] {
  position: absolute;
  pointer-events: none;
}
[data-rmiz-btn-zoom],
[data-rmiz-btn-unzoom] {
  background-color: rgba(0, 0, 0, 0.7);
  border-radius: 50%;
  border: none;
  box-shadow: 0 0 1px rgba(255, 255, 255, 0.5);
  color: #fff;
  height: 40px;
  margin: 0;
  outline-offset: 2px;
  padding: 9px;
  touch-action: manipulation;
  width: 40px;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
}
[data-rmiz-btn-zoom]:not(:focus):not(:active) {
  position: absolute;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  pointer-events: none;
  white-space: nowrap;
  width: 1px;
}
[data-rmiz-btn-zoom] {
  position: absolute;
  inset: 10px 10px auto auto;
  cursor: zoom-in;
}
[data-rmiz-btn-unzoom] {
  position: absolute;
  inset: 20px 20px auto auto;
  cursor: zoom-out;
  z-index: 1;
}
[data-rmiz-content='found'] img,
[data-rmiz-content='found'] svg,
[data-rmiz-content='found'] [role='img'],
[data-rmiz-content='found'] [data-zoom] {
  cursor: inherit;
}
[data-rmiz-modal]::backdrop {
  display: none;
}
[data-rmiz-modal][open] {
  position: fixed;
  width: 100vw;
  width: 100dvw;
  height: 100vh;
  height: 100dvh;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  overflow: hidden;
}
[data-rmiz-modal-overlay] {
  position: absolute;
  inset: 0;
  transition: background-color 0.3s;
}
[data-rmiz-modal-overlay='hidden'] {
  background-color: rgba(255, 255, 255, 0);
}
[data-rmiz-modal-overlay='visible'] {
  background-color: rgba(255, 255, 255, 1);
}
[data-rmiz-modal-content] {
  position: relative;
  width: 100%;
  height: 100%;
}
[data-rmiz-modal-img] {
  position: absolute;
  cursor: zoom-out;
  image-rendering: high-quality;
  transform-origin: top left;
  transition: transform 0.3s;
}
@media (prefers-reduced-motion: reduce) {
  [data-rmiz-modal-overlay],
  [data-rmiz-modal-img] {
    transition-duration: 0.01ms !important;
  }
}
</file>

<file path="src/components/rich-editor/styles/index.css">
@reference "../../../assets/styles/globals.css";

@import './partials/code.css';
@import './partials/placeholder.css';
@import './partials/lists.css';
@import './partials/typography.css';
@import './partials/zoom.css';

:root {
  --mt-overlay: rgba(251, 251, 251, 0.75);
  --mt-transparent-foreground: rgba(0, 0, 0, 0.4);
  --mt-bg-secondary: rgba(251, 251, 251, 0.8);
  --mt-code-background: #082b781f;
  --mt-code-color: #d4d4d4;
  --mt-secondary: #9d9d9f;
  --mt-pre-background: #ececec;
  --mt-pre-border: #e0e0e0;
  --mt-pre-color: #2f2f31;
  --mt-hr: #dcdcdc;
  --mt-drag-handle-hover: #5c5c5e;

  --mt-accent-bold-blue: #05c;
  --mt-accent-bold-teal: #206a83;
  --mt-accent-bold-green: #216e4e;
  --mt-accent-bold-orange: #a54800;
  --mt-accent-bold-red: #ae2e24;
  --mt-accent-bold-purple: #5e4db2;

  --mt-accent-gray: #758195;
  --mt-accent-blue: #1d7afc;
  --mt-accent-teal: #2898bd;
  --mt-accent-green: #22a06b;
  --mt-accent-orange: #fea362;
  --mt-accent-red: #c9372c;
  --mt-accent-purple: #8270db;

  --mt-accent-blue-subtler: #cce0ff;
  --mt-accent-teal-subtler: #c6edfb;
  --mt-accent-green-subtler: #baf3db;
  --mt-accent-yellow-subtler: #f8e6a0;
  --mt-accent-red-subtler: #ffd5d2;
  --mt-accent-purple-subtler: #dfd8fd;

  --hljs-string: #aa430f;
  --hljs-title: #b08836;
  --hljs-comment: #999999;
  --hljs-keyword: #0c5eb1;
  --hljs-attr: #3a92bc;
  --hljs-literal: #c82b0f;
  --hljs-name: #259792;
  --hljs-selector-tag: #c8500f;
  --hljs-number: #3da067;
}

.dark {
  --mt-overlay: rgba(31, 32, 35, 0.75);
  --mt-transparent-foreground: rgba(255, 255, 255, 0.4);
  --mt-bg-secondary: rgba(31, 32, 35, 0.8);
  --mt-code-background: #ffffff13;
  --mt-code-color: #2c2e33;
  --mt-secondary: #595a5c;
  --mt-pre-background: #080808;
  --mt-pre-border: #23252a;
  --mt-pre-color: #e3e4e6;
  --mt-hr: #26282d;
  --mt-drag-handle-hover: #969799;

  --mt-accent-bold-blue: #85b8ff;
  --mt-accent-bold-teal: #9dd9ee;
  --mt-accent-bold-green: #7ee2b8;
  --mt-accent-bold-orange: #fec195;
  --mt-accent-bold-red: #fd9891;
  --mt-accent-bold-purple: #b8acf6;

  --mt-accent-gray: #738496;
  --mt-accent-blue: #388bff;
  --mt-accent-teal: #42b2d7;
  --mt-accent-green: #2abb7f;
  --mt-accent-orange: #a54800;
  --mt-accent-red: #e2483d;
  --mt-accent-purple: #8f7ee7;

  --mt-accent-blue-subtler: #09326c;
  --mt-accent-teal-subtler: #164555;
  --mt-accent-green-subtler: #164b35;
  --mt-accent-yellow-subtler: #533f04;
  --mt-accent-red-subtler: #5d1f1a;
  --mt-accent-purple-subtler: #352c63;

  --hljs-string: #da936b;
  --hljs-title: #f1d59d;
  --hljs-comment: #aaaaaa;
  --hljs-keyword: #6699cc;
  --hljs-attr: #90cae8;
  --hljs-literal: #f2777a;
  --hljs-name: #5fc0a0;
  --hljs-selector-tag: #e8c785;
  --hljs-number: #b6e7b6;
}

.minimal-tiptap-editor .ProseMirror {
  @apply flex max-w-full cursor-text flex-col;
  @apply z-0 outline-0;
}

.minimal-tiptap-editor .ProseMirror > div.editor {
  @apply block flex-1 whitespace-pre-wrap;
}

.minimal-tiptap-editor .ProseMirror .block-node:not(:last-child),
.minimal-tiptap-editor .ProseMirror .list-node:not(:last-child),
.minimal-tiptap-editor .ProseMirror .text-node:not(:last-child) {
  @apply mb-2.5;
}

.minimal-tiptap-editor .ProseMirror ol,
.minimal-tiptap-editor .ProseMirror ul {
  @apply pl-6;
}

.minimal-tiptap-editor .ProseMirror blockquote,
.minimal-tiptap-editor .ProseMirror dl,
.minimal-tiptap-editor .ProseMirror ol,
.minimal-tiptap-editor .ProseMirror p,
.minimal-tiptap-editor .ProseMirror pre,
.minimal-tiptap-editor .ProseMirror ul {
  @apply m-0;
}

.minimal-tiptap-editor .ProseMirror li {
  @apply leading-7;
}

.minimal-tiptap-editor .ProseMirror p {
  @apply break-words;
}

.minimal-tiptap-editor .ProseMirror li .text-node:has(+ .list-node),
.minimal-tiptap-editor .ProseMirror li > .list-node,
.minimal-tiptap-editor .ProseMirror li > .text-node,
.minimal-tiptap-editor .ProseMirror li p {
  @apply mb-0;
}

.minimal-tiptap-editor .ProseMirror blockquote {
  @apply relative pl-3.5;
}

.minimal-tiptap-editor .ProseMirror blockquote::before,
.minimal-tiptap-editor .ProseMirror blockquote.is-empty::before {
  @apply absolute bottom-0 left-0 top-0 h-full w-1 rounded-sm bg-accent-foreground/15 content-[''];
}

.minimal-tiptap-editor .ProseMirror hr {
  @apply my-3 h-0.5 w-full border-none bg-[var(--mt-hr)];
}

.minimal-tiptap-editor .ProseMirror-focused hr.ProseMirror-selectednode {
  @apply rounded-full outline outline-2 outline-offset-1 outline-muted-foreground;
}

.minimal-tiptap-editor .ProseMirror .ProseMirror-gapcursor {
  @apply pointer-events-none absolute hidden;
}

.minimal-tiptap-editor .ProseMirror .ProseMirror-hideselection {
  @apply caret-transparent;
}

.minimal-tiptap-editor .ProseMirror.resize-cursor {
  @apply cursor-col-resize;
}

.minimal-tiptap-editor .ProseMirror .selection {
  @apply inline-block;
}

.minimal-tiptap-editor .ProseMirror s span {
  @apply line-through;
}

.minimal-tiptap-editor .ProseMirror .selection,
.minimal-tiptap-editor .ProseMirror *::selection,
::selection {
  @apply bg-primary/25;
}

/* Override native selection when custom selection is present */
.minimal-tiptap-editor .ProseMirror .selection::selection {
  background: transparent;
}
</file>

<file path="src/components/rich-editor/index.ts">
export * from './minimal-tiptap';
</file>

<file path="src/components/rich-editor/minimal-tiptap.tsx">
import './styles/index.css';

import type { Content, Editor } from '@tiptap/react';
import { EditorContent } from '@tiptap/react';
import * as React from 'react';

import { Separator } from '@/components/ui/separator';

import { cn } from '@/lib/utils';

import { LinkBubbleMenu } from './components/bubble-menu/link-bubble-menu';
import { MeasuredContainer } from './components/measured-container';
import { SectionFive } from './components/section/five';
import { SectionFour } from './components/section/four';
import { SectionOne } from './components/section/one';
import { SectionThree } from './components/section/three';
import { SectionTwo } from './components/section/two';
import { useMinimalTiptapEditor } from './hooks/use-minimal-tiptap';
import type { UseMinimalTiptapEditorProps } from './hooks/use-minimal-tiptap';

export interface MinimalTiptapProps extends Omit<UseMinimalTiptapEditorProps, 'onUpdate'> {
  value?: Content;
  onChange?: (value: Content) => void;
  className?: string;
  editorContentClassName?: string;
  maxHeight?: string;
}

const Toolbar = ({ editor }: { editor: Editor }) => (
  <div className="shrink-0 overflow-x-auto border-b border-border p-2">
    <div className="flex flex-wrap max-w-max items-center gap-px">
      <SectionOne editor={editor} activeLevels={[1, 2, 3, 4, 5, 6]} />

      <Separator orientation="vertical" className="mx-2 !h-7" />

      <SectionTwo
        editor={editor}
        activeActions={['bold', 'italic', 'underline', 'strikethrough', 'code', 'clearFormatting']}
        mainActionCount={3}
      />

      <Separator orientation="vertical" className="mx-2 !h-7" />

      <SectionThree editor={editor} />

      <Separator orientation="vertical" className="mx-2 !h-7" />

      <SectionFour
        editor={editor}
        activeActions={['orderedList', 'bulletList']}
        mainActionCount={0}
      />

      <Separator orientation="vertical" className="mx-2 !h-7" />

      <SectionFive
        editor={editor}
        activeActions={['codeBlock', 'blockquote', 'horizontalRule']}
        mainActionCount={0}
      />
    </div>
  </div>
);

export const MinimalTiptapEditor = React.forwardRef<HTMLDivElement, MinimalTiptapProps>(
  ({ value, onChange, maxHeight, className, editorContentClassName, ...props }, ref) => {
    const editor = useMinimalTiptapEditor({
      value,
      onUpdate: onChange,
      ...props,
    });

    if (!editor) {
      return null;
    }

    return (
      <MeasuredContainer
        as="div"
        name="editor"
        ref={ref}
        className={cn(
          'flex h-auto min-h-72 w-full flex-col rounded-md border border-input shadow-xs bg-card focus-within:border-ring focus-within:ring-ring/15 transition-[color,box-shadow] outline-none focus-within:ring-[3px]',
          'group-aria-[invalid=true]:!ring-destructive/20 dark:group-aria-[invalid=true]:!ring-destructive/40 group-aria-[invalid=true]:!border-destructive',
          className,
        )}
      >
        <Toolbar editor={editor} />
        <EditorContent
          editor={editor}
          className={cn('minimal-tiptap-editor overflow-auto h-full', editorContentClassName, {
            'max-h-[300px]': maxHeight,
          })}
        />
        <LinkBubbleMenu editor={editor} />
      </MeasuredContainer>
    );
  },
);

MinimalTiptapEditor.displayName = 'MinimalTiptapEditor';

export default MinimalTiptapEditor;
</file>

<file path="src/components/rich-editor/types.ts">
import type { EditorState } from '@tiptap/pm/state';
import type { EditorView } from '@tiptap/pm/view';
import type { Editor } from '@tiptap/react';

export interface LinkProps {
  url: string;
  text?: string;
  openInNewTab?: boolean;
}

export interface ShouldShowProps {
  editor: Editor;
  view: EditorView;
  state: EditorState;
  oldState?: EditorState;
  from: number;
  to: number;
}

export interface FormatAction {
  label: string;
  icon?: React.ReactNode;
  action: (editor: Editor) => void;
  isActive: (editor: Editor) => boolean;
  canExecute: (editor: Editor) => boolean;
  shortcuts: string[];
  value: string;
}
</file>

<file path="src/components/rich-editor/utils.ts">
import type { Editor } from '@tiptap/react';

import type { MinimalTiptapProps } from './minimal-tiptap';

type ShortcutKeyResult = {
  symbol: string;
  readable: string;
};

export type FileError = {
  file: File | string;
  reason: 'type' | 'size' | 'invalidBase64' | 'base64NotAllowed';
};

export type FileValidationOptions = {
  allowedMimeTypes: string[];
  maxFileSize?: number;
  allowBase64: boolean;
};

type FileInput = File | { src: string | File; alt?: string; title?: string };

export const isClient = (): boolean => typeof window !== 'undefined';
export const isServer = (): boolean => !isClient();
export const isMacOS = (): boolean => isClient() && window.navigator.platform === 'MacIntel';

const shortcutKeyMap: Record<string, ShortcutKeyResult> = {
  mod: isMacOS() ? { symbol: '⌘', readable: 'Command' } : { symbol: 'Ctrl', readable: 'Control' },
  alt: isMacOS() ? { symbol: '⌥', readable: 'Option' } : { symbol: 'Alt', readable: 'Alt' },
  shift: { symbol: '⇧', readable: 'Shift' },
};

export const getShortcutKey = (key: string): ShortcutKeyResult =>
  shortcutKeyMap[key.toLowerCase()] || { symbol: key, readable: key };

export const getShortcutKeys = (keys: string[]): ShortcutKeyResult[] => keys.map(getShortcutKey);

export const getOutput = (
  editor: Editor,
  format: MinimalTiptapProps['output'],
): object | string => {
  switch (format) {
    case 'json':
      return editor.getJSON();
    case 'html':
      return editor.isEmpty ? '' : editor.getHTML();
    default:
      return editor.getText();
  }
};

export const isUrl = (
  text: string,
  options: { requireHostname: boolean; allowBase64?: boolean } = { requireHostname: false },
): boolean => {
  if (text.includes('\n')) return false;

  try {
    const url = new URL(text);
    const blockedProtocols = [
      'javascript:',
      'file:',
      'vbscript:',
      ...(options.allowBase64 ? [] : ['data:']),
    ];

    if (blockedProtocols.includes(url.protocol)) return false;
    if (options.allowBase64 && url.protocol === 'data:')
      return /^data:image\/[a-z]+;base64,/.test(text);
    if (url.hostname) return true;

    return (
      url.protocol !== '' &&
      (url.pathname.startsWith('//') || url.pathname.startsWith('http')) &&
      !options.requireHostname
    );
  } catch {
    return false;
  }
};

export const sanitizeUrl = (
  url: string | null | undefined,
  options: { allowBase64?: boolean } = {},
): string | undefined => {
  if (!url) return undefined;

  if (options.allowBase64 && url.startsWith('data:image')) {
    return isUrl(url, { requireHostname: false, allowBase64: true }) ? url : undefined;
  }

  return isUrl(url, { requireHostname: false, allowBase64: options.allowBase64 }) ||
    /^(\/|#|mailto:|sms:|fax:|tel:)/.test(url)
    ? url
    : `https://${url}`;
};

export const blobUrlToBase64 = async (blobUrl: string): Promise<string> => {
  const response = await fetch(blobUrl);
  const blob = await response.blob();

  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onloadend = () => {
      if (typeof reader.result === 'string') {
        resolve(reader.result);
      } else {
        reject(new Error('Failed to convert Blob to base64'));
      }
    };
    reader.onerror = reject;
    reader.readAsDataURL(blob);
  });
};

export const randomId = (): string => Math.random().toString(36).slice(2, 11);

export const fileToBase64 = (file: File | Blob): Promise<string> => {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onloadend = () => {
      if (typeof reader.result === 'string') {
        resolve(reader.result);
      } else {
        reject(new Error('Failed to convert File to base64'));
      }
    };
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
};

const validateFileOrBase64 = <T extends FileInput>(
  input: File | string,
  options: FileValidationOptions,
  originalFile: T,
  validFiles: T[],
  errors: FileError[],
): void => {
  const { isValidType, isValidSize } = checkTypeAndSize(input, options);

  if (isValidType && isValidSize) {
    validFiles.push(originalFile);
  } else {
    if (!isValidType) errors.push({ file: input, reason: 'type' });
    if (!isValidSize) errors.push({ file: input, reason: 'size' });
  }
};

const checkTypeAndSize = (
  input: File | string,
  { allowedMimeTypes, maxFileSize }: FileValidationOptions,
): { isValidType: boolean; isValidSize: boolean } => {
  const mimeType = input instanceof File ? input.type : base64MimeType(input);
  const size = input instanceof File ? input.size : atob(input.split(',')[1]).length;

  const isValidType =
    allowedMimeTypes.length === 0 ||
    allowedMimeTypes.includes(mimeType) ||
    allowedMimeTypes.includes(`${mimeType.split('/')[0]}/*`);

  const isValidSize = !maxFileSize || size <= maxFileSize;

  return { isValidType, isValidSize };
};

const base64MimeType = (encoded: string): string => {
  const result = encoded.match(/data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+).*,.*/);

  return result && result.length > 1 ? result[1] : 'unknown';
};

const isBase64 = (str: string): boolean => {
  if (str.startsWith('data:')) {
    const matches = str.match(/^data:[^;]+;base64,(.+)$/);
    if (matches && matches[1]) {
      str = matches[1];
    } else {
      return false;
    }
  }

  try {
    return btoa(atob(str)) === str;
  } catch {
    return false;
  }
};

export const filterFiles = <T extends FileInput>(
  files: T[],
  options: FileValidationOptions,
): [T[], FileError[]] => {
  const validFiles: T[] = [];
  const errors: FileError[] = [];

  files.forEach((file) => {
    const actualFile = 'src' in file ? file.src : file;

    if (actualFile instanceof File) {
      validateFileOrBase64(actualFile, options, file, validFiles, errors);
    } else if (typeof actualFile === 'string') {
      if (isBase64(actualFile)) {
        if (options.allowBase64) {
          validateFileOrBase64(actualFile, options, file, validFiles, errors);
        } else {
          errors.push({ file: actualFile, reason: 'base64NotAllowed' });
        }
      } else {
        if (!sanitizeUrl(actualFile, { allowBase64: options.allowBase64 })) {
          errors.push({ file: actualFile, reason: 'invalidBase64' });
        } else {
          validFiles.push(file);
        }
      }
    }
  });

  return [validFiles, errors];
};
</file>

<file path="src/components/skeletons/form-skeletons.tsx">
import React from 'react';

import { Skeleton } from '../ui/skeleton';

export const FormInputSkeletons = () => {
  return (
    <div className="space-y-2 flex-1">
      <Skeleton className="w-24 h-4 rounded-sm" />
      <Skeleton className="w-full h-10" />
    </div>
  );
};

export const FormTextareaSkeletons = () => {
  return (
    <div className="space-y-2 flex-1">
      <Skeleton className="w-24 h-4 rounded-sm" />
      <Skeleton className="w-full h-16" />
    </div>
  );
};
</file>

<file path="src/components/skeletons/table-skeleton.tsx">
import React from 'react';

import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';

import { Skeleton } from '../ui/skeleton';

const TableSkeletons = () => {
  return (
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead className="w-[100px]">
            <Skeleton className="w-24 h-4 rounded-sm" />
          </TableHead>
          <TableHead>
            <Skeleton className="w-24 h-4 rounded-sm" />
          </TableHead>
          <TableHead>
            <Skeleton className="w-24 h-4 rounded-sm" />
          </TableHead>
          <TableHead>
            <Skeleton className="w-24 h-4 rounded-sm" />
          </TableHead>
          <TableHead>
            <Skeleton className="w-24 h-4 rounded-sm" />
          </TableHead>
          <TableHead>
            <Skeleton className="w-24 h-4 rounded-sm" />
          </TableHead>
          <TableHead>
            <Skeleton className="w-24 h-4 rounded-sm" />
          </TableHead>
          <TableHead className="text-right">
            <Skeleton className="w-24 h-4 rounded-sm" />
          </TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {Array.from({ length: 20 }).map((_, index) => (
          <TableRow key={index}>
            <TableCell>
              <Skeleton className="w-24 h-4 rounded-sm" />
            </TableCell>
            <TableCell>
              <Skeleton className="w-24 h-4 rounded-sm" />
            </TableCell>
            <TableCell>
              <Skeleton className="w-24 h-4 rounded-sm" />
            </TableCell>
            <TableCell>
              <Skeleton className="w-24 h-4 rounded-sm" />
            </TableCell>
            <TableCell>
              <Skeleton className="w-24 h-4 rounded-sm" />
            </TableCell>
            <TableCell>
              <Skeleton className="w-24 h-4 rounded-sm" />
            </TableCell>
            <TableCell>
              <Skeleton className="w-24 h-4 rounded-sm" />
            </TableCell>
            <TableCell className="text-right">
              <Skeleton className="w-24 h-4 rounded-sm" />
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
};

export default TableSkeletons;
</file>

<file path="src/components/ui/accordion.tsx">
'use client';

import * as AccordionPrimitive from '@radix-ui/react-accordion';
import { ChevronDownIcon } from 'lucide-react';
import * as React from 'react';

import { cn } from '@/lib/utils';

function Accordion({ ...props }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
  return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
}

function AccordionItem({
  className,
  ...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
  return (
    <AccordionPrimitive.Item
      data-slot="accordion-item"
      className={cn('border-b last:border-b-0', className)}
      {...props}
    />
  );
}

function AccordionTrigger({
  className,
  children,
  ...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
  return (
    <AccordionPrimitive.Header className="flex">
      <AccordionPrimitive.Trigger
        data-slot="accordion-trigger"
        className={cn(
          'focus-visible:border-ring focus-visible:ring-ring/15 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-70 [&[data-state=open]>svg]:rotate-180',
          className,
        )}
        {...props}
      >
        {children}
        <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
      </AccordionPrimitive.Trigger>
    </AccordionPrimitive.Header>
  );
}

function AccordionContent({
  className,
  children,
  ...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
  return (
    <AccordionPrimitive.Content
      data-slot="accordion-content"
      className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
      {...props}
    >
      <div className={cn('pt-0 pb-4', className)}>{children}</div>
    </AccordionPrimitive.Content>
  );
}

export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
</file>

<file path="src/components/ui/alert-dialog.tsx">
'use client';

import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
import * as React from 'react';

import { buttonVariants } from '@/components/ui/button';

import { cn } from '@/lib/utils';

function AlertDialog({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
  return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
}

function AlertDialogTrigger({
  ...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
  return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />;
}

function AlertDialogPortal({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
  return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />;
}

function AlertDialogOverlay({
  className,
  ...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
  return (
    <AlertDialogPrimitive.Overlay
      data-slot="alert-dialog-overlay"
      className={cn(
        'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
        className,
      )}
      {...props}
    />
  );
}

function AlertDialogContent({
  className,
  ...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
  return (
    <AlertDialogPortal>
      <AlertDialogOverlay />
      <AlertDialogPrimitive.Content
        data-slot="alert-dialog-content"
        className={cn(
          'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
          className,
        )}
        {...props}
      />
    </AlertDialogPortal>
  );
}

function AlertDialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot="alert-dialog-header"
      className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
      {...props}
    />
  );
}

function AlertDialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot="alert-dialog-footer"
      className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
      {...props}
    />
  );
}

function AlertDialogTitle({
  className,
  ...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
  return (
    <AlertDialogPrimitive.Title
      data-slot="alert-dialog-title"
      className={cn('text-lg font-semibold', className)}
      {...props}
    />
  );
}

function AlertDialogDescription({
  className,
  ...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
  return (
    <AlertDialogPrimitive.Description
      data-slot="alert-dialog-description"
      className={cn('text-muted-foreground text-sm', className)}
      {...props}
    />
  );
}

function AlertDialogAction({
  className,
  ...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
  return <AlertDialogPrimitive.Action className={cn(buttonVariants(), className)} {...props} />;
}

function AlertDialogCancel({
  className,
  ...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
  return (
    <AlertDialogPrimitive.Cancel
      className={cn(buttonVariants({ variant: 'outline' }), className)}
      {...props}
    />
  );
}

export {
  AlertDialog,
  AlertDialogPortal,
  AlertDialogOverlay,
  AlertDialogTrigger,
  AlertDialogContent,
  AlertDialogHeader,
  AlertDialogFooter,
  AlertDialogTitle,
  AlertDialogDescription,
  AlertDialogAction,
  AlertDialogCancel,
};
</file>

<file path="src/components/ui/alert.tsx">
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';

import { cn } from '@/lib/utils';

const alertVariants = cva(
  'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
  {
    variants: {
      variant: {
        default: 'bg-background text-foreground',
        destructive:
          'text-destructive-foreground [&>svg]:text-current *:data-[slot=alert-description]:text-destructive-foreground/80',
      },
    },
    defaultVariants: {
      variant: 'default',
    },
  },
);

function Alert({
  className,
  variant,
  ...props
}: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
  return (
    <div
      data-slot="alert"
      role="alert"
      className={cn(alertVariants({ variant }), className)}
      {...props}
    />
  );
}

function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot="alert-title"
      className={cn('col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight', className)}
      {...props}
    />
  );
}

function AlertDescription({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot="alert-description"
      className={cn(
        'text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed',
        className,
      )}
      {...props}
    />
  );
}

export { Alert, AlertTitle, AlertDescription };
</file>

<file path="src/components/ui/aspect-ratio.tsx">
'use client';

import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio';

function AspectRatio({ ...props }: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
  return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />;
}

export { AspectRatio };
</file>

<file path="src/components/ui/avatar.tsx">
'use client';

import * as AvatarPrimitive from '@radix-ui/react-avatar';
import * as React from 'react';

import { cn } from '@/lib/utils';

const defaultColors = ['#A62A21', '#7e3794', '#0B51C1', '#3A6024', '#A81563', '#B3003C'];

function _stringAsciiPRNG(value: string, m: number) {
  const charCodes = [...value].map((letter) => letter.charCodeAt(0));
  const len = charCodes.length;

  const a = (len % (m - 1)) + 1;
  const c = charCodes.reduce((current, next) => current + next) % m;

  let random = charCodes[0] % m;
  for (let i = 0; i < len; i++) random = (a * random + c) % m;

  return random;
}

export function getRandomColor(value: string, colors = defaultColors) {
  // if no value is passed, always return transparent color otherwise
  // a rerender would show a new color which would will
  // give strange effects when an interface is loading
  // and gets rerendered a few consequent times
  if (!value) return 'transparent';

  // value based random color index
  // the reason we don't just use a random number is to make sure that
  // a certain value will always get the same color assigned given
  // a fixed set of colors
  const colorIndex = _stringAsciiPRNG(value, colors.length);

  return colors[colorIndex];
}

export function getNameInitials(name: string) {
  if (!name || name.length < 3) return '';

  const [firstName, lastName] = name.split(' ');

  if (firstName && lastName) {
    return `${firstName.charAt(0)}${lastName.charAt(0)}`;
  }

  return `${firstName.charAt(0)}${firstName.charAt(1)}`;
}

const AvatarWrapper = React.forwardRef<
  React.ComponentRef<typeof AvatarPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
  <AvatarPrimitive.Root
    ref={ref}
    className={cn('relative flex h-9 w-9 shrink-0 overflow-hidden rounded-md', className)}
    {...props}
  />
));
AvatarWrapper.displayName = AvatarPrimitive.Root.displayName;

const AvatarImage = React.forwardRef<
  React.ComponentRef<typeof AvatarPrimitive.Image>,
  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
  <AvatarPrimitive.Image
    ref={ref}
    className={cn('aspect-square object-cover h-full w-full', className)}
    {...props}
  />
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;

const AvatarFallback = React.forwardRef<
  React.ComponentRef<typeof AvatarPrimitive.Fallback>,
  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
  <AvatarPrimitive.Fallback
    ref={ref}
    className={cn(
      'flex h-full w-full items-center justify-center rounded-full bg-muted',
      className,
    )}
    {...props}
  />
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;

const Avatar = React.forwardRef<
  React.ComponentRef<typeof AvatarPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> & {
    src?: string;
    name: string;
  }
>(({ src, name, ...props }, ref) => {
  return (
    <AvatarWrapper {...props} ref={ref}>
      <AvatarImage src={src} alt={name} />
      <AvatarFallback
        className="rounded-lg text-white"
        style={{
          backgroundColor: getRandomColor(name),
        }}
      >
        {getNameInitials(name)}
      </AvatarFallback>
    </AvatarWrapper>
  );
});
Avatar.displayName = 'Avatar';

export { AvatarWrapper, AvatarImage, AvatarFallback, Avatar };
</file>

<file path="src/components/ui/badge.tsx">
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';

import { cn } from '@/lib/utils';

const badgeVariants = cva(
  'inline-flex items-center justify-center rounded-full border px-2.5 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/15 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
  {
    variants: {
      variant: {
        default: 'border-primary/30 bg-primary/10 text-primary',
        secondary:
          'border-border bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
        destructive: 'border-destructive/30 bg-destructive/10 text-destructive',
        success: 'border-success/30 bg-success/10 text-success',
        outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
      },
    },
    defaultVariants: {
      variant: 'default',
    },
  },
);

function Badge({
  className,
  variant,
  asChild = false,
  ...props
}: React.ComponentProps<'span'> & VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
  const Comp = asChild ? Slot : 'span';

  return (
    <Comp data-slot="badge" className={cn(badgeVariants({ variant }), className)} {...props} />
  );
}

export { Badge, badgeVariants };
</file>

<file path="src/components/ui/breadcrumb.tsx">
import { Slot } from '@radix-ui/react-slot';
import { ChevronRight, MoreHorizontal } from 'lucide-react';
import * as React from 'react';

import { cn } from '@/lib/utils';

function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) {
  return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
}

function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
  return (
    <ol
      data-slot="breadcrumb-list"
      className={cn(
        'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5',
        className,
      )}
      {...props}
    />
  );
}

function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
  return (
    <li
      data-slot="breadcrumb-item"
      className={cn('inline-flex items-center gap-1.5', className)}
      {...props}
    />
  );
}

function BreadcrumbLink({
  asChild,
  className,
  ...props
}: React.ComponentProps<'a'> & {
  asChild?: boolean;
}) {
  const Comp = asChild ? Slot : 'a';

  return (
    <Comp
      data-slot="breadcrumb-link"
      className={cn('hover:text-foreground transition-colors', className)}
      {...props}
    />
  );
}

function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
  return (
    <span
      data-slot="breadcrumb-page"
      role="link"
      aria-disabled="true"
      aria-current="page"
      className={cn('text-foreground font-normal', className)}
      {...props}
    />
  );
}

function BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<'li'>) {
  return (
    <li
      data-slot="breadcrumb-separator"
      role="presentation"
      aria-hidden="true"
      className={cn('[&>svg]:size-3.5', className)}
      {...props}
    >
      {children ?? <ChevronRight />}
    </li>
  );
}

function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<'span'>) {
  return (
    <span
      data-slot="breadcrumb-ellipsis"
      role="presentation"
      aria-hidden="true"
      className={cn('flex size-9 items-center justify-center', className)}
      {...props}
    >
      <MoreHorizontal className="size-4" />
      <span className="sr-only">More</span>
    </span>
  );
}

export {
  Breadcrumb,
  BreadcrumbList,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbPage,
  BreadcrumbSeparator,
  BreadcrumbEllipsis,
};
</file>

<file path="src/components/ui/button.tsx">
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';

import { cn } from '@/lib/utils';

const buttonVariants = cva(
  "inline-flex items-center cursor-pointer justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow,background-color] disabled:pointer-events-none disabled:opacity-70 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/15 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
        destructive:
          'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40',
        outline:
          'border border-input bg-card shadow-xs hover:bg-accent hover:text-accent-foreground',
        secondary: 'bg-secondary shadow-xs hover:bg-secondary/60 border border-input',
        ghost: 'hover:bg-accent hover:text-accent-foreground',
        link: 'text-primary underline-offset-4 hover:underline',
      },
      size: {
        default: 'h-9 px-3 py-2 has-[>svg]:px-3',
        sm: 'h-6 rounded-sm text-xs gap-1.5 px-3 has-[>svg]:px-2.5',
        lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
        icon: 'size-9',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
    },
  },
);

function Button({
  className,
  variant,
  size,
  type = 'button',
  asChild = false,
  ...props
}: React.ComponentProps<'button'> &
  VariantProps<typeof buttonVariants> & {
    asChild?: boolean;
  }) {
  const Comp = asChild ? Slot : 'button';

  return (
    <Comp
      data-slot="button"
      type={type}
      className={cn(buttonVariants({ variant, size, className }))}
      {...props}
    />
  );
}

export { Button, buttonVariants };
</file>

<file path="src/components/ui/calendar.tsx">
'use client';

import { ChevronLeft, ChevronRight } from 'lucide-react';
import * as React from 'react';
import { DayPicker } from 'react-day-picker';

import { buttonVariants } from '@/components/ui/button';

import { cn } from '@/lib/utils';

function Calendar({
  className,
  classNames,
  showOutsideDays = true,
  ...props
}: React.ComponentProps<typeof DayPicker>) {
  return (
    <DayPicker
      showOutsideDays={showOutsideDays}
      className={cn('p-3', className)}
      classNames={{
        months: 'flex flex-col sm:flex-row gap-2',
        month: 'flex flex-col gap-4',
        caption: 'flex justify-center pt-1 relative items-center w-full',
        caption_label: 'text-sm font-medium',
        nav: 'flex items-center gap-1',
        nav_button: cn(
          buttonVariants({ variant: 'outline' }),
          'size-7 bg-transparent p-0 opacity-50 hover:opacity-100',
        ),
        nav_button_previous: 'absolute left-1',
        nav_button_next: 'absolute right-1',
        table: 'w-full border-collapse space-x-1',
        head_row: 'flex',
        head_cell: 'text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]',
        row: 'flex w-full mt-2',
        cell: cn(
          'relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md',
          props.mode === 'range'
            ? '[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md'
            : '[&:has([aria-selected])]:rounded-md',
        ),
        day: cn(
          buttonVariants({ variant: 'ghost' }),
          'size-8 p-0 font-normal aria-selected:opacity-100',
        ),
        day_range_start:
          'day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground',
        day_range_end:
          'day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground',
        day_selected:
          'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
        day_today: 'bg-accent text-accent-foreground',
        day_outside: 'day-outside text-muted-foreground aria-selected:text-muted-foreground',
        day_disabled: 'text-muted-foreground opacity-50',
        day_range_middle: 'aria-selected:bg-accent aria-selected:text-accent-foreground',
        day_hidden: 'invisible',
        ...classNames,
      }}
      components={{
        IconLeft: ({ className, ...props }) => (
          <ChevronLeft className={cn('size-4', className)} {...props} />
        ),
        IconRight: ({ className, ...props }) => (
          <ChevronRight className={cn('size-4', className)} {...props} />
        ),
      }}
      {...props}
    />
  );
}

export { Calendar };
</file>

<file path="src/components/ui/card.tsx">
import * as React from 'react';

import { cn } from '@/lib/utils';

function Card({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot="card"
      className={cn(
        'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border shadow-sm',
        className,
      )}
      {...props}
    />
  );
}

function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot="card-header"
      className={cn('flex flex-col gap-1.5 px-6', className)}
      {...props}
    />
  );
}

function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot="card-title"
      className={cn('leading-none font-semibold', className)}
      {...props}
    />
  );
}

function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot="card-description"
      className={cn('text-muted-foreground text-sm', className)}
      {...props}
    />
  );
}

function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
  return <div data-slot="card-content" className={cn('px-6', className)} {...props} />;
}

function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div data-slot="card-footer" className={cn('flex items-center px-6', className)} {...props} />
  );
}

export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
</file>

<file path="src/components/ui/carousel.tsx">
'use client';

import useEmblaCarousel, { type UseEmblaCarouselType } from 'embla-carousel-react';
import { ArrowLeft, ArrowRight } from 'lucide-react';
import * as React from 'react';

import { Button } from '@/components/ui/button';

import { cn } from '@/lib/utils';

type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];

type CarouselProps = {
  opts?: CarouselOptions;
  plugins?: CarouselPlugin;
  orientation?: 'horizontal' | 'vertical';
  setApi?: (api: CarouselApi) => void;
};

type CarouselContextProps = {
  carouselRef: ReturnType<typeof useEmblaCarousel>[0];
  api: ReturnType<typeof useEmblaCarousel>[1];
  scrollPrev: () => void;
  scrollNext: () => void;
  canScrollPrev: boolean;
  canScrollNext: boolean;
} & CarouselProps;

const CarouselContext = React.createContext<CarouselContextProps | null>(null);

function useCarousel() {
  const context = React.useContext(CarouselContext);

  if (!context) {
    throw new Error('useCarousel must be used within a <Carousel />');
  }

  return context;
}

function Carousel({
  orientation = 'horizontal',
  opts,
  setApi,
  plugins,
  className,
  children,
  ...props
}: React.ComponentProps<'div'> & CarouselProps) {
  const [carouselRef, api] = useEmblaCarousel(
    {
      ...opts,
      axis: orientation === 'horizontal' ? 'x' : 'y',
    },
    plugins,
  );
  const [canScrollPrev, setCanScrollPrev] = React.useState(false);
  const [canScrollNext, setCanScrollNext] = React.useState(false);

  const onSelect = React.useCallback((api: CarouselApi) => {
    if (!api) return;
    setCanScrollPrev(api.canScrollPrev());
    setCanScrollNext(api.canScrollNext());
  }, []);

  const scrollPrev = React.useCallback(() => {
    api?.scrollPrev();
  }, [api]);

  const scrollNext = React.useCallback(() => {
    api?.scrollNext();
  }, [api]);

  const handleKeyDown = React.useCallback(
    (event: React.KeyboardEvent<HTMLDivElement>) => {
      if (event.key === 'ArrowLeft') {
        event.preventDefault();
        scrollPrev();
      } else if (event.key === 'ArrowRight') {
        event.preventDefault();
        scrollNext();
      }
    },
    [scrollPrev, scrollNext],
  );

  React.useEffect(() => {
    if (!api || !setApi) return;
    setApi(api);
  }, [api, setApi]);

  React.useEffect(() => {
    if (!api) return;
    onSelect(api);
    api.on('reInit', onSelect);
    api.on('select', onSelect);

    return () => {
      api?.off('select', onSelect);
    };
  }, [api, onSelect]);

  return (
    <CarouselContext.Provider
      value={{
        carouselRef,
        api: api,
        opts,
        orientation: orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
        scrollPrev,
        scrollNext,
        canScrollPrev,
        canScrollNext,
      }}
    >
      <div
        onKeyDownCapture={handleKeyDown}
        className={cn('relative', className)}
        role="region"
        aria-roledescription="carousel"
        data-slot="carousel"
        {...props}
      >
        {children}
      </div>
    </CarouselContext.Provider>
  );
}

function CarouselContent({ className, ...props }: React.ComponentProps<'div'>) {
  const { carouselRef, orientation } = useCarousel();

  return (
    <div ref={carouselRef} className="overflow-hidden" data-slot="carousel-content">
      <div
        className={cn('flex', orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col', className)}
        {...props}
      />
    </div>
  );
}

function CarouselItem({ className, ...props }: React.ComponentProps<'div'>) {
  const { orientation } = useCarousel();

  return (
    <div
      role="group"
      aria-roledescription="slide"
      data-slot="carousel-item"
      className={cn(
        'min-w-0 shrink-0 grow-0 basis-full',
        orientation === 'horizontal' ? 'pl-4' : 'pt-4',
        className,
      )}
      {...props}
    />
  );
}

function CarouselPrevious({
  className,
  variant = 'outline',
  size = 'icon',
  ...props
}: React.ComponentProps<typeof Button>) {
  const { orientation, scrollPrev, canScrollPrev } = useCarousel();

  return (
    <Button
      data-slot="carousel-previous"
      variant={variant}
      size={size}
      className={cn(
        'absolute size-8 rounded-full',
        orientation === 'horizontal'
          ? 'top-1/2 -left-12 -translate-y-1/2'
          : '-top-12 left-1/2 -translate-x-1/2 rotate-90',
        className,
      )}
      disabled={!canScrollPrev}
      onClick={scrollPrev}
      {...props}
    >
      <ArrowLeft />
      <span className="sr-only">Previous slide</span>
    </Button>
  );
}

function CarouselNext({
  className,
  variant = 'outline',
  size = 'icon',
  ...props
}: React.ComponentProps<typeof Button>) {
  const { orientation, scrollNext, canScrollNext } = useCarousel();

  return (
    <Button
      data-slot="carousel-next"
      variant={variant}
      size={size}
      className={cn(
        'absolute size-8 rounded-full',
        orientation === 'horizontal'
          ? 'top-1/2 -right-12 -translate-y-1/2'
          : '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
        className,
      )}
      disabled={!canScrollNext}
      onClick={scrollNext}
      {...props}
    >
      <ArrowRight />
      <span className="sr-only">Next slide</span>
    </Button>
  );
}

export {
  type CarouselApi,
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselPrevious,
  CarouselNext,
};
</file>

<file path="src/components/ui/chart.tsx">
'use client';

import * as React from 'react';
import * as RechartsPrimitive from 'recharts';

import { cn } from '@/lib/utils';

// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: '', dark: '.dark' } as const;

export type ChartConfig = {
  [k in string]: {
    label?: React.ReactNode;
    icon?: React.ComponentType;
  } & (
    | { color?: string; theme?: never }
    | { color?: never; theme: Record<keyof typeof THEMES, string> }
  );
};

type ChartContextProps = {
  config: ChartConfig;
};

const ChartContext = React.createContext<ChartContextProps | null>(null);

function useChart() {
  const context = React.useContext(ChartContext);

  if (!context) {
    throw new Error('useChart must be used within a <ChartContainer />');
  }

  return context;
}

function ChartContainer({
  id,
  className,
  children,
  config,
  ...props
}: React.ComponentProps<'div'> & {
  config: ChartConfig;
  children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>['children'];
}) {
  const uniqueId = React.useId();
  const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`;

  return (
    <ChartContext.Provider value={{ config }}>
      <div
        data-slot="chart"
        data-chart={chartId}
        className={cn(
          "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
          className,
        )}
        {...props}
      >
        <ChartStyle id={chartId} config={config} />
        <RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
      </div>
    </ChartContext.Provider>
  );
}

const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
  const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color);

  if (!colorConfig.length) {
    return null;
  }

  return (
    <style
      dangerouslySetInnerHTML={{
        __html: Object.entries(THEMES)
          .map(
            ([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
  .map(([key, itemConfig]) => {
    const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;

    return color ? `  --color-${key}: ${color};` : null;
  })
  .join('\n')}
}
`,
          )
          .join('\n'),
      }}
    />
  );
};

const ChartTooltip = RechartsPrimitive.Tooltip;

function ChartTooltipContent({
  active,
  payload,
  className,
  indicator = 'dot',
  hideLabel = false,
  hideIndicator = false,
  label,
  labelFormatter,
  labelClassName,
  formatter,
  color,
  nameKey,
  labelKey,
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
  React.ComponentProps<'div'> & {
    hideLabel?: boolean;
    hideIndicator?: boolean;
    indicator?: 'line' | 'dot' | 'dashed';
    nameKey?: string;
    labelKey?: string;
  }) {
  const { config } = useChart();

  const tooltipLabel = React.useMemo(() => {
    if (hideLabel || !payload?.length) {
      return null;
    }

    const [item] = payload;
    const key = `${labelKey || item?.dataKey || item?.name || 'value'}`;
    const itemConfig = getPayloadConfigFromPayload(config, item, key);
    const value =
      !labelKey && typeof label === 'string'
        ? config[label as keyof typeof config]?.label || label
        : itemConfig?.label;

    if (labelFormatter) {
      return (
        <div className={cn('font-medium', labelClassName)}>{labelFormatter(value, payload)}</div>
      );
    }

    if (!value) {
      return null;
    }

    return <div className={cn('font-medium', labelClassName)}>{value}</div>;
  }, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);

  if (!active || !payload?.length) {
    return null;
  }

  const nestLabel = payload.length === 1 && indicator !== 'dot';

  return (
    <div
      className={cn(
        'border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl',
        className,
      )}
    >
      {!nestLabel ? tooltipLabel : null}
      <div className="grid gap-1.5">
        {payload.map((item, index) => {
          const key = `${nameKey || item.name || item.dataKey || 'value'}`;
          const itemConfig = getPayloadConfigFromPayload(config, item, key);
          const indicatorColor = color || item.payload.fill || item.color;

          return (
            <div
              key={item.dataKey}
              className={cn(
                '[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5',
                indicator === 'dot' && 'items-center',
              )}
            >
              {formatter && item?.value !== undefined && item.name ? (
                formatter(item.value, item.name, item, index, item.payload)
              ) : (
                <>
                  {itemConfig?.icon ? (
                    <itemConfig.icon />
                  ) : (
                    !hideIndicator && (
                      <div
                        className={cn(
                          'shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)',
                          {
                            'h-2.5 w-2.5': indicator === 'dot',
                            'w-1': indicator === 'line',
                            'w-0 border-[1.5px] border-dashed bg-transparent':
                              indicator === 'dashed',
                            'my-0.5': nestLabel && indicator === 'dashed',
                          },
                        )}
                        style={
                          {
                            '--color-bg': indicatorColor,
                            '--color-border': indicatorColor,
                          } as React.CSSProperties
                        }
                      />
                    )
                  )}
                  <div
                    className={cn(
                      'flex flex-1 justify-between leading-none',
                      nestLabel ? 'items-end' : 'items-center',
                    )}
                  >
                    <div className="grid gap-1.5">
                      {nestLabel ? tooltipLabel : null}
                      <span className="text-muted-foreground">
                        {itemConfig?.label || item.name}
                      </span>
                    </div>
                    {item.value && (
                      <span className="text-foreground font-mono font-medium tabular-nums">
                        {item.value.toLocaleString()}
                      </span>
                    )}
                  </div>
                </>
              )}
            </div>
          );
        })}
      </div>
    </div>
  );
}

const ChartLegend = RechartsPrimitive.Legend;

function ChartLegendContent({
  className,
  hideIcon = false,
  payload,
  verticalAlign = 'bottom',
  nameKey,
}: React.ComponentProps<'div'> &
  Pick<RechartsPrimitive.LegendProps, 'payload' | 'verticalAlign'> & {
    hideIcon?: boolean;
    nameKey?: string;
  }) {
  const { config } = useChart();

  if (!payload?.length) {
    return null;
  }

  return (
    <div
      className={cn(
        'flex items-center justify-center gap-4',
        verticalAlign === 'top' ? 'pb-3' : 'pt-3',
        className,
      )}
    >
      {payload.map((item) => {
        const key = `${nameKey || item.dataKey || 'value'}`;
        const itemConfig = getPayloadConfigFromPayload(config, item, key);

        return (
          <div
            key={item.value}
            className={cn(
              '[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3',
            )}
          >
            {itemConfig?.icon && !hideIcon ? (
              <itemConfig.icon />
            ) : (
              <div
                className="h-2 w-2 shrink-0 rounded-[2px]"
                style={{
                  backgroundColor: item.color,
                }}
              />
            )}
            {itemConfig?.label}
          </div>
        );
      })}
    </div>
  );
}

// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
  if (typeof payload !== 'object' || payload === null) {
    return undefined;
  }

  const payloadPayload =
    'payload' in payload && typeof payload.payload === 'object' && payload.payload !== null
      ? payload.payload
      : undefined;

  let configLabelKey: string = key;

  if (key in payload && typeof payload[key as keyof typeof payload] === 'string') {
    configLabelKey = payload[key as keyof typeof payload] as string;
  } else if (
    payloadPayload &&
    key in payloadPayload &&
    typeof payloadPayload[key as keyof typeof payloadPayload] === 'string'
  ) {
    configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
  }

  return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
}

export {
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
  ChartLegend,
  ChartLegendContent,
  ChartStyle,
};
</file>

<file path="src/components/ui/checkbox.tsx">
'use client';

import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { CheckIcon, MinusIcon } from 'lucide-react';
import * as React from 'react';

import { cn } from '@/lib/utils';

function Checkbox({
  className,
  isIndeterminate,
  ...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root> & { isIndeterminate?: boolean }) {
  return (
    <CheckboxPrimitive.Root
      data-slot="checkbox"
      className={cn(
        'peer border-foreground/30 cursor-pointer data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/15 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-[17px] shrink-0 rounded-[5px] border transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-70',
        className,
      )}
      {...props}
    >
      <CheckboxPrimitive.Indicator
        data-slot="checkbox-indicator"
        className="flex items-center justify-center text-current transition-none"
      >
        {isIndeterminate ? <MinusIcon className="size-3" /> : <CheckIcon className="size-3" />}
      </CheckboxPrimitive.Indicator>
    </CheckboxPrimitive.Root>
  );
}

export { Checkbox };
</file>

<file path="src/components/ui/circular-progress.tsx">
'use client';

import type React from 'react';
import { useState, useEffect } from 'react';

import { cn } from '@/lib/utils';

interface CircularProgressProps {
  value: number;
  max?: number;
  size?: number;
  strokeWidth?: number;
  className?: string;
  labelClassName?: string;
  showValue?: boolean;
  valuePrefix?: string;
  valueSuffix?: string;
  color?: string;
  bgColor?: string;
  label?: React.ReactNode;
}

export default function CircularProgress({
  value,
  max = 100,
  size = 120,
  strokeWidth = 8,
  className,
  labelClassName,
  showValue = true,
  valuePrefix = '',
  valueSuffix = '%',
  color = 'var(--primary)',
  bgColor = 'var(--muted)',
  label,
}: CircularProgressProps) {
  const [progress, setProgress] = useState(0);

  // Animation effect
  useEffect(() => {
    const timer = setTimeout(() => {
      setProgress(value);
    }, 100);

    return () => clearTimeout(timer);
  }, [value]);

  // Calculate SVG parameters
  const normalizedValue = Math.min(Math.max(progress, 0), max);
  const percentage = (normalizedValue / max) * 100;
  const radius = (size - strokeWidth) / 2;
  const circumference = radius * 2 * Math.PI;
  const strokeDashoffset = circumference - (percentage / 100) * circumference;

  return (
    <div
      className={cn('relative inline-flex items-center justify-center', className)}
      style={{ width: size, height: size }}
    >
      <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} className="rotate-[-90deg]">
        {/* Background circle */}
        <circle
          cx={size / 2}
          cy={size / 2}
          r={radius}
          strokeWidth={strokeWidth}
          className={`fill-none`}
          style={{ stroke: bgColor }}
        />

        {/* Progress circle */}
        <circle
          cx={size / 2}
          cy={size / 2}
          r={radius}
          strokeWidth={strokeWidth}
          className={`fill-none transition-all duration-500 ease-out`}
          style={{ stroke: color }}
          strokeDasharray={circumference}
          strokeDashoffset={strokeDashoffset}
          strokeLinecap="round"
        />
      </svg>

      {/* Center label */}
      <div
        className={cn(
          'absolute inset-0 flex items-center justify-center flex-col text-center',
          labelClassName,
        )}
      >
        {showValue && (
          <span className="font-medium">
            {valuePrefix}
            {Math.round(percentage)}
            {valueSuffix}
          </span>
        )}
        {label && <span className="text-sm text-muted-foreground">{label}</span>}
      </div>
    </div>
  );
}
</file>

<file path="src/components/ui/collapsible.tsx">
'use client';

import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';

function Collapsible({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
  return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
}

function CollapsibleTrigger({
  ...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
  return <CollapsiblePrimitive.CollapsibleTrigger data-slot="collapsible-trigger" {...props} />;
}

function CollapsibleContent({
  ...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
  return <CollapsiblePrimitive.CollapsibleContent data-slot="collapsible-content" {...props} />;
}

export { Collapsible, CollapsibleTrigger, CollapsibleContent };
</file>

<file path="src/components/ui/combobox.tsx">
'use client';

import { Check, ChevronsUpDown } from 'lucide-react';
import * as React from 'react';

import { Button } from '@/components/ui/button';
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from '@/components/ui/command';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';

import { cn } from '@/lib/utils';

export function Combobox({
  value,
  placeholder,
  onChange,
  options,
}: {
  value?: string;
  placeholder?: string;
  onChange: (value?: string) => void;
  options: { label: string; value: string }[];
}) {
  const [open, setOpen] = React.useState(false);

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button
          variant="outline"
          role="combobox"
          aria-expanded={open}
          className="h-10 justify-between !pr-2"
        >
          {value ? options.find((item) => item.value === value)?.label : placeholder || 'Select...'}
          <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
        </Button>
      </PopoverTrigger>
      <PopoverContent className="p-0 w-[var(--radix-popover-trigger-width)]">
        <Command>
          <CommandInput placeholder="Search..." />
          <CommandList>
            <CommandEmpty>No options found.</CommandEmpty>
            <CommandGroup>
              {options.map((item) => (
                <CommandItem
                  key={item.value}
                  value={item.label}
                  onSelect={() => {
                    onChange(item.value);
                    setOpen(false);
                  }}
                >
                  <Check
                    className={cn(
                      'mr-2 h-4 w-4',
                      value === item.value ? 'opacity-100' : 'opacity-0',
                    )}
                  />
                  {item.label}
                </CommandItem>
              ))}
            </CommandGroup>
          </CommandList>
        </Command>
      </PopoverContent>
    </Popover>
  );
}
</file>

<file path="src/components/ui/command.tsx">
'use client';

import { Command as CommandPrimitive } from 'cmdk';
import { SearchIcon } from 'lucide-react';
import * as React from 'react';

import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
} from '@/components/ui/dialog';

import { cn } from '@/lib/utils';

function Command({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) {
  return (
    <CommandPrimitive
      data-slot="command"
      className={cn(
        'bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md',
        className,
      )}
      {...props}
    />
  );
}

function CommandDialog({
  title = 'Command Palette',
  description = 'Search for a command to run...',
  children,
  ...props
}: React.ComponentProps<typeof Dialog> & {
  title?: string;
  description?: string;
}) {
  return (
    <Dialog {...props}>
      <DialogHeader className="sr-only">
        <DialogTitle>{title}</DialogTitle>
        <DialogDescription>{description}</DialogDescription>
      </DialogHeader>
      <DialogContent className="overflow-hidden p-0">
        <Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
          {children}
        </Command>
      </DialogContent>
    </Dialog>
  );
}

function CommandInput({
  className,
  ...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
  return (
    <div data-slot="command-input-wrapper" className="flex items-center gap-2 border-b px-3">
      <SearchIcon className="size-4 shrink-0 opacity-50" />
      <CommandPrimitive.Input
        data-slot="command-input"
        className={cn(
          'placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-70',
          className,
        )}
        {...props}
      />
    </div>
  );
}

function CommandList({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.List>) {
  return (
    <CommandPrimitive.List
      data-slot="command-list"
      className={cn('max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto', className)}
      {...props}
    />
  );
}

function CommandEmpty({ ...props }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
  return (
    <CommandPrimitive.Empty
      data-slot="command-empty"
      className="py-6 text-center text-sm"
      {...props}
    />
  );
}

function CommandGroup({
  className,
  ...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
  return (
    <CommandPrimitive.Group
      data-slot="command-group"
      className={cn(
        'text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium',
        className,
      )}
      {...props}
    />
  );
}

function CommandSeparator({
  className,
  ...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
  return (
    <CommandPrimitive.Separator
      data-slot="command-separator"
      className={cn('bg-border -mx-1 h-px', className)}
      {...props}
    />
  );
}

function CommandItem({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Item>) {
  return (
    <CommandPrimitive.Item
      data-slot="command-item"
      className={cn(
        "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className,
      )}
      {...props}
    />
  );
}

function CommandShortcut({ className, ...props }: React.ComponentProps<'span'>) {
  return (
    <span
      data-slot="command-shortcut"
      className={cn('text-muted-foreground ml-auto text-xs tracking-widest', className)}
      {...props}
    />
  );
}

export {
  Command,
  CommandDialog,
  CommandInput,
  CommandList,
  CommandEmpty,
  CommandGroup,
  CommandItem,
  CommandShortcut,
  CommandSeparator,
};
</file>

<file path="src/components/ui/context-menu.tsx">
'use client';

import * as ContextMenuPrimitive from '@radix-ui/react-context-menu';
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
import * as React from 'react';

import { cn } from '@/lib/utils';

function ContextMenu({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
  return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
}

function ContextMenuTrigger({
  ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
  return <ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />;
}

function ContextMenuGroup({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
  return <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />;
}

function ContextMenuPortal({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
  return <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />;
}

function ContextMenuSub({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
  return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
}

function ContextMenuRadioGroup({
  ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
  return <ContextMenuPrimitive.RadioGroup data-slot="context-menu-radio-group" {...props} />;
}

function ContextMenuSubTrigger({
  className,
  inset,
  children,
  ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
  inset?: boolean;
}) {
  return (
    <ContextMenuPrimitive.SubTrigger
      data-slot="context-menu-sub-trigger"
      data-inset={inset}
      className={cn(
        "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className,
      )}
      {...props}
    >
      {children}
      <ChevronRightIcon className="ml-auto" />
    </ContextMenuPrimitive.SubTrigger>
  );
}

function ContextMenuSubContent({
  className,
  ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
  return (
    <ContextMenuPrimitive.SubContent
      data-slot="context-menu-sub-content"
      className={cn(
        'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg',
        className,
      )}
      {...props}
    />
  );
}

function ContextMenuContent({
  className,
  ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
  return (
    <ContextMenuPrimitive.Portal>
      <ContextMenuPrimitive.Content
        data-slot="context-menu-content"
        className={cn(
          'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md',
          className,
        )}
        {...props}
      />
    </ContextMenuPrimitive.Portal>
  );
}

function ContextMenuItem({
  className,
  inset,
  variant = 'default',
  ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
  inset?: boolean;
  variant?: 'default' | 'destructive';
}) {
  return (
    <ContextMenuPrimitive.Item
      data-slot="context-menu-item"
      data-inset={inset}
      data-variant={variant}
      className={cn(
        "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive-foreground data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/40 data-[variant=destructive]:focus:text-destructive-foreground data-[variant=destructive]:*:[svg]:!text-destructive-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className,
      )}
      {...props}
    />
  );
}

function ContextMenuCheckboxItem({
  className,
  children,
  checked,
  ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
  return (
    <ContextMenuPrimitive.CheckboxItem
      data-slot="context-menu-checkbox-item"
      className={cn(
        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className,
      )}
      checked={checked}
      {...props}
    >
      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
        <ContextMenuPrimitive.ItemIndicator>
          <CheckIcon className="size-4" />
        </ContextMenuPrimitive.ItemIndicator>
      </span>
      {children}
    </ContextMenuPrimitive.CheckboxItem>
  );
}

function ContextMenuRadioItem({
  className,
  children,
  ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
  return (
    <ContextMenuPrimitive.RadioItem
      data-slot="context-menu-radio-item"
      className={cn(
        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className,
      )}
      {...props}
    >
      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
        <ContextMenuPrimitive.ItemIndicator>
          <CircleIcon className="size-2 fill-current" />
        </ContextMenuPrimitive.ItemIndicator>
      </span>
      {children}
    </ContextMenuPrimitive.RadioItem>
  );
}

function ContextMenuLabel({
  className,
  inset,
  ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
  inset?: boolean;
}) {
  return (
    <ContextMenuPrimitive.Label
      data-slot="context-menu-label"
      data-inset={inset}
      className={cn('text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', className)}
      {...props}
    />
  );
}

function ContextMenuSeparator({
  className,
  ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
  return (
    <ContextMenuPrimitive.Separator
      data-slot="context-menu-separator"
      className={cn('bg-border -mx-1 my-1 h-px', className)}
      {...props}
    />
  );
}

function ContextMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) {
  return (
    <span
      data-slot="context-menu-shortcut"
      className={cn('text-muted-foreground ml-auto text-xs tracking-widest', className)}
      {...props}
    />
  );
}

export {
  ContextMenu,
  ContextMenuTrigger,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuCheckboxItem,
  ContextMenuRadioItem,
  ContextMenuLabel,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuGroup,
  ContextMenuPortal,
  ContextMenuSub,
  ContextMenuSubContent,
  ContextMenuSubTrigger,
  ContextMenuRadioGroup,
};
</file>

<file path="src/components/ui/copy-button.tsx">
import { Copy, Check } from 'lucide-react';
import { useState, useEffect } from 'react';

import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';

import { cn } from '@/lib/utils';

interface CopyButtonProps {
  textToCopy: string;
  className?: string;
  tooltipTitle?: string;
}

export default function CopyButton({ textToCopy, className, tooltipTitle }: CopyButtonProps) {
  const [isCopied, setIsCopied] = useState(false);

  useEffect(() => {
    if (isCopied) {
      const timer = setTimeout(() => setIsCopied(false), 2000);

      return () => clearTimeout(timer);
    }

    return undefined;
  }, [isCopied]);

  const handleCopy = async () => {
    try {
      await navigator.clipboard.writeText(textToCopy);
      setIsCopied(true);
    } catch (err) {
      console.error('Failed to copy text: ', err);
    }
  };

  return (
    <Tooltip>
      <TooltipTrigger asChild>
        <Button
          variant="ghost"
          size="icon"
          onClick={handleCopy}
          className={cn('h-8 w-8', className)}
        >
          {isCopied ? <Check className="size-4 " /> : <Copy className="size-4" />}
          <span className="sr-only">{tooltipTitle || 'Copy to clipboard'}</span>
        </Button>
      </TooltipTrigger>
      <TooltipContent>
        <p>{isCopied ? 'Copied!' : tooltipTitle || 'Copy to clipboard'}</p>
      </TooltipContent>
    </Tooltip>
  );
}
</file>

<file path="src/components/ui/datetime-picker.tsx">
'use client';

import { CalendarIcon } from '@radix-ui/react-icons';
import { format } from 'date-fns';

import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';

import { cn } from '@/lib/utils';

export function DateTimePicker({
  value,
  onChange,
}: {
  value?: Date;
  onChange: (date: Date) => void;
}) {
  function handleDateSelect(date: Date | undefined) {
    if (date) {
      onChange(date);
    }
  }

  function handleTimeChange(type: 'hour' | 'minute' | 'ampm', v: string) {
    const currentDate = value || new Date();
    const newDate = new Date(currentDate);

    if (type === 'hour') {
      const hour = parseInt(v, 10);
      newDate.setHours(newDate.getHours() >= 12 ? hour + 12 : hour);
    } else if (type === 'minute') {
      newDate.setMinutes(parseInt(v, 10));
    } else if (type === 'ampm') {
      const hours = newDate.getHours();
      if (v === 'AM' && hours >= 12) {
        newDate.setHours(hours - 12);
      } else if (v === 'PM' && hours < 12) {
        newDate.setHours(hours + 12);
      }
    }

    onChange(newDate);
  }

  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button
          variant={'outline'}
          className={cn(
            'w-full pl-3 h-10 text-left font-normal bg-card shadow-xs',
            !value && 'text-muted-foreground',
          )}
        >
          {value ? format(value, 'MM/dd/yyyy hh:mm aa') : <span>MM/DD/YYYY hh:mm aa</span>}
          <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
        </Button>
      </PopoverTrigger>
      <PopoverContent className="w-auto p-0">
        <div className="sm:flex">
          <Calendar mode="single" selected={value} onSelect={handleDateSelect} initialFocus />
          <div className="flex flex-col sm:flex-row sm:h-[300px] divide-y sm:divide-y-0 sm:divide-x">
            <ScrollArea className="w-64 sm:w-auto">
              <div className="flex sm:flex-col p-2">
                {Array.from({ length: 12 }, (_, i) => i + 1)
                  .reverse()
                  .map((hour) => (
                    <Button
                      key={hour}
                      size="icon"
                      variant={value && value.getHours() % 12 === hour % 12 ? 'default' : 'ghost'}
                      className="sm:w-full shrink-0 aspect-square size-8"
                      onClick={() => handleTimeChange('hour', hour.toString())}
                    >
                      {hour}
                    </Button>
                  ))}
              </div>
              <ScrollBar orientation="horizontal" className="sm:hidden" />
            </ScrollArea>
            <ScrollArea className="w-64 sm:w-auto">
              <div className="flex sm:flex-col p-2">
                {Array.from({ length: 12 }, (_, i) => i * 5).map((minute) => (
                  <Button
                    key={minute}
                    size="icon"
                    variant={value && value.getMinutes() === minute ? 'default' : 'ghost'}
                    className="sm:w-full shrink-0 aspect-square size-8"
                    onClick={() => handleTimeChange('minute', minute.toString())}
                  >
                    {minute.toString().padStart(2, '0')}
                  </Button>
                ))}
              </div>
              <ScrollBar orientation="horizontal" className="sm:hidden" />
            </ScrollArea>
            <ScrollArea className="">
              <div className="flex sm:flex-col p-2">
                {['AM', 'PM'].map((ampm) => (
                  <Button
                    key={ampm}
                    size="icon"
                    variant={
                      value &&
                      ((ampm === 'AM' && value.getHours() < 12) ||
                        (ampm === 'PM' && value.getHours() >= 12))
                        ? 'default'
                        : 'ghost'
                    }
                    className="sm:w-full shrink-0 aspect-square size-8"
                    onClick={() => handleTimeChange('ampm', ampm)}
                  >
                    {ampm}
                  </Button>
                ))}
              </div>
            </ScrollArea>
          </div>
        </div>
      </PopoverContent>
    </Popover>
  );
}
</file>

<file path="src/components/ui/delete-alert.tsx">
import { Loader } from 'lucide-react';
import React from 'react';

import { Button } from '@/components/ui/button';
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from '@/components/ui/dialog';

function DeleteAlert({
  open,
  onClose,
  onDelete,
  isLoading,
  description,
}: {
  open: boolean;
  onClose: () => void;
  onDelete: () => void;
  isLoading: boolean;
  description?: string;
}) {
  return (
    <Dialog
      open={open}
      onOpenChange={(e) => {
        if (!e) {
          onClose();
        }
      }}
    >
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Are you absolutely sure?</DialogTitle>
          <DialogDescription>
            {description || 'This action cannot be undone. This will permanently deleted.'}
          </DialogDescription>
        </DialogHeader>
        <DialogFooter>
          <Button variant="ghost" onClick={onClose}>
            Cancel
          </Button>
          <Button
            onClick={onDelete}
            variant="destructive"
            className="min-w-[100px]"
            disabled={isLoading}
          >
            {isLoading ? <Loader className="animate-spin" /> : 'Delete'}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

export default DeleteAlert;
</file>

<file path="src/components/ui/dialog.tsx">
'use client';

import * as DialogPrimitive from '@radix-ui/react-dialog';
import { XIcon } from 'lucide-react';
import * as React from 'react';

import { cn } from '@/lib/utils';

function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
  return <DialogPrimitive.Root data-slot="dialog" {...props} />;
}

function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
  return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}

function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
  return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}

function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
  return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}

function DialogOverlay({
  className,
  ...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
  return (
    <DialogPrimitive.Overlay
      data-slot="dialog-overlay"
      className={cn(
        'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
        className,
      )}
      {...props}
    />
  );
}

function DialogContent({
  className,
  children,
  ...props
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
  return (
    <DialogPortal data-slot="dialog-portal">
      <DialogOverlay />
      <DialogPrimitive.Content
        data-slot="dialog-content"
        className={cn(
          'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-xl border p-6 flex flex-col shadow-lg duration-200 sm:max-w-lg max-h-[calc(100svh-30px)] overflow-y-auto',
          className,
        )}
        {...props}
      >
        {children}
        <DialogPrimitive.Close className="cursor-pointer focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute bg-background rounded-[5px] top-4 right-4 p-0.5 opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
          <XIcon />
          <span className="sr-only">Close</span>
        </DialogPrimitive.Close>
      </DialogPrimitive.Content>
    </DialogPortal>
  );
}

function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div data-slot="dialog-header" className={cn('flex flex-col gap-2', className)} {...props} />
  );
}

function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot="dialog-footer"
      className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
      {...props}
    />
  );
}

function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
  return (
    <DialogPrimitive.Title
      data-slot="dialog-title"
      className={cn('text-lg leading-none font-semibold', className)}
      {...props}
    />
  );
}

function DialogDescription({
  className,
  ...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
  return (
    <DialogPrimitive.Description
      data-slot="dialog-description"
      className={cn('text-muted-foreground text-sm', className)}
      {...props}
    />
  );
}

export {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogOverlay,
  DialogPortal,
  DialogTitle,
  DialogTrigger,
};
</file>

<file path="src/components/ui/drawer.tsx">
'use client';

import * as React from 'react';
import { Drawer as DrawerPrimitive } from 'vaul';

import { cn } from '@/lib/utils';

function Drawer({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Root>) {
  return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
}

function DrawerTrigger({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
  return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
}

function DrawerPortal({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
  return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
}

function DrawerClose({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Close>) {
  return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
}

function DrawerOverlay({
  className,
  ...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
  return (
    <DrawerPrimitive.Overlay
      data-slot="drawer-overlay"
      className={cn(
        'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
        className,
      )}
      {...props}
    />
  );
}

function DrawerContent({
  className,
  children,
  ...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
  return (
    <DrawerPortal data-slot="drawer-portal">
      <DrawerOverlay />
      <DrawerPrimitive.Content
        data-slot="drawer-content"
        className={cn(
          'group/drawer-content bg-background fixed z-50 flex h-auto flex-col',
          'data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg',
          'data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg',
          'data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:sm:max-w-sm',
          'data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:sm:max-w-sm',
          className,
        )}
        {...props}
      >
        <div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
        {children}
      </DrawerPrimitive.Content>
    </DrawerPortal>
  );
}

function DrawerHeader({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot="drawer-header"
      className={cn('flex flex-col gap-1.5 p-4', className)}
      {...props}
    />
  );
}

function DrawerFooter({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot="drawer-footer"
      className={cn('mt-auto flex flex-col gap-2 p-4', className)}
      {...props}
    />
  );
}

function DrawerTitle({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Title>) {
  return (
    <DrawerPrimitive.Title
      data-slot="drawer-title"
      className={cn('text-foreground font-semibold', className)}
      {...props}
    />
  );
}

function DrawerDescription({
  className,
  ...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
  return (
    <DrawerPrimitive.Description
      data-slot="drawer-description"
      className={cn('text-muted-foreground text-sm', className)}
      {...props}
    />
  );
}

export {
  Drawer,
  DrawerPortal,
  DrawerOverlay,
  DrawerTrigger,
  DrawerClose,
  DrawerContent,
  DrawerHeader,
  DrawerFooter,
  DrawerTitle,
  DrawerDescription,
};
</file>

<file path="src/components/ui/dropdown-menu.tsx">
'use client';

import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
import * as React from 'react';

import { cn } from '@/lib/utils';

function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
  return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}

function DropdownMenuPortal({
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
  return <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
}

function DropdownMenuTrigger({
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
  return (
    <DropdownMenuPrimitive.Trigger
      className={cn('cursor-pointer', props.className)}
      data-slot="dropdown-menu-trigger"
      {...props}
    />
  );
}

function DropdownMenuContent({
  className,
  sideOffset = 4,
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
  return (
    <DropdownMenuPrimitive.Portal>
      <DropdownMenuPrimitive.Content
        data-slot="dropdown-menu-content"
        sideOffset={sideOffset}
        onCloseAutoFocus={(e) => e.preventDefault()}
        className={cn(
          'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
          className,
        )}
        {...props}
      />
    </DropdownMenuPrimitive.Portal>
  );
}

function DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
  return <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
}

function DropdownMenuItem({
  className,
  inset,
  variant = 'default',
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
  inset?: boolean;
  variant?: 'default' | 'destructive';
}) {
  return (
    <DropdownMenuPrimitive.Item
      data-slot="dropdown-menu-item"
      data-inset={inset}
      data-variant={variant}
      className={cn(
        "focus:bg-accent cursor-pointer rounded-md focus:text-accent-foreground data-[variant=destructive]:text-destructive-foreground data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/40 data-[variant=destructive]:focus:text-destructive-foreground data-[variant=destructive]:*:[svg]:!text-destructive-foreground [&_svg:not([class*='text-'])]:text-inherit relative flex items-center gap-2 px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className,
      )}
      {...props}
    />
  );
}

function DropdownMenuCheckboxItem({
  className,
  children,
  checked,
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
  return (
    <DropdownMenuPrimitive.CheckboxItem
      data-slot="dropdown-menu-checkbox-item"
      className={cn(
        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className,
      )}
      checked={checked}
      {...props}
    >
      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
        <DropdownMenuPrimitive.ItemIndicator>
          <CheckIcon className="size-4" />
        </DropdownMenuPrimitive.ItemIndicator>
      </span>
      {children}
    </DropdownMenuPrimitive.CheckboxItem>
  );
}

function DropdownMenuRadioGroup({
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
  return <DropdownMenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
}

function DropdownMenuRadioItem({
  className,
  children,
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
  return (
    <DropdownMenuPrimitive.RadioItem
      data-slot="dropdown-menu-radio-item"
      className={cn(
        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className,
      )}
      {...props}
    >
      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
        <DropdownMenuPrimitive.ItemIndicator>
          <CircleIcon className="size-2 fill-current" />
        </DropdownMenuPrimitive.ItemIndicator>
      </span>
      {children}
    </DropdownMenuPrimitive.RadioItem>
  );
}

function DropdownMenuLabel({
  className,
  inset,
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
  inset?: boolean;
}) {
  return (
    <DropdownMenuPrimitive.Label
      data-slot="dropdown-menu-label"
      data-inset={inset}
      className={cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', className)}
      {...props}
    />
  );
}

function DropdownMenuSeparator({
  className,
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
  return (
    <DropdownMenuPrimitive.Separator
      data-slot="dropdown-menu-separator"
      className={cn('bg-border -mx-1 my-1 h-px', className)}
      {...props}
    />
  );
}

function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) {
  return (
    <span
      data-slot="dropdown-menu-shortcut"
      className={cn('text-muted-foreground ml-auto text-xs tracking-widest', className)}
      {...props}
    />
  );
}

function DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
  return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
}

function DropdownMenuSubTrigger({
  className,
  inset,
  children,
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
  inset?: boolean;
}) {
  return (
    <DropdownMenuPrimitive.SubTrigger
      data-slot="dropdown-menu-sub-trigger"
      data-inset={inset}
      className={cn(
        'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
        className,
      )}
      {...props}
    >
      {children}
      <ChevronRightIcon className="ml-auto size-4" />
    </DropdownMenuPrimitive.SubTrigger>
  );
}

function DropdownMenuSubContent({
  className,
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
  return (
    <DropdownMenuPrimitive.SubContent
      data-slot="dropdown-menu-sub-content"
      className={cn(
        'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg',
        className,
      )}
      {...props}
    />
  );
}

export {
  DropdownMenu,
  DropdownMenuPortal,
  DropdownMenuTrigger,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuLabel,
  DropdownMenuItem,
  DropdownMenuCheckboxItem,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuSub,
  DropdownMenuSubTrigger,
  DropdownMenuSubContent,
};
</file>

<file path="src/components/ui/form.tsx">
'use client';

import * as LabelPrimitive from '@radix-ui/react-label';
import { Slot } from '@radix-ui/react-slot';
import * as React from 'react';
import {
  Controller,
  FormProvider,
  useFormContext,
  useFormState,
  type ControllerProps,
  type FieldPath,
  type FieldValues,
} from 'react-hook-form';

import { Label } from '@/components/ui/label';

import { cn } from '@/lib/utils';

const Form = FormProvider;

type FormFieldContextValue<
  TFieldValues extends FieldValues = FieldValues,
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
  name: TName;
  isRequired?: boolean;
};

const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);

const FormField = <
  TFieldValues extends FieldValues = FieldValues,
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
  ...props
}: ControllerProps<TFieldValues, TName> & {
  isRequired?: boolean;
}) => {
  return (
    <FormFieldContext.Provider value={{ name: props.name, isRequired: props.isRequired }}>
      <Controller {...props} />
    </FormFieldContext.Provider>
  );
};

const useFormField = () => {
  const fieldContext = React.useContext(FormFieldContext);
  const itemContext = React.useContext(FormItemContext);
  const { getFieldState } = useFormContext();
  const formState = useFormState({ name: fieldContext.name });
  const fieldState = getFieldState(fieldContext.name, formState);

  if (!fieldContext) {
    throw new Error('useFormField should be used within <FormField>');
  }

  const { id } = itemContext;

  return {
    id,
    name: fieldContext.name,
    formItemId: `${id}-form-item`,
    formDescriptionId: `${id}-form-item-description`,
    formMessageId: `${id}-form-item-message`,
    isRequired: fieldContext.isRequired,
    ...fieldState,
  };
};

type FormItemContextValue = {
  id: string;
};

const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);

function FormItem({ className, ...props }: React.ComponentProps<'div'>) {
  const id = React.useId();

  return (
    <FormItemContext.Provider value={{ id }}>
      <div data-slot="form-item" className={cn('grid space-y-3', className)} {...props} />
    </FormItemContext.Provider>
  );
}

function FormLabel({
  className,
  children,
  ...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
  const { error, formItemId, isRequired } = useFormField();

  return (
    <Label
      data-slot="form-label"
      data-error={!!error}
      className={cn('data-[error=true]:text-destructive gap-1', className)}
      htmlFor={formItemId}
      {...props}
    >
      {children}
      {isRequired && (
        <span className="text-destructive" aria-hidden>
          {' '}
          *
        </span>
      )}
    </Label>
  );
}

function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
  const { error, formItemId, formDescriptionId, formMessageId } = useFormField();

  return (
    <Slot
      data-slot="form-control"
      id={formItemId}
      aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
      aria-invalid={!!error}
      {...props}
    />
  );
}

function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
  const { formDescriptionId } = useFormField();

  return (
    <p
      data-slot="form-description"
      id={formDescriptionId}
      className={cn('text-muted-foreground text-sm', className)}
      {...props}
    />
  );
}

function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
  const { error, formMessageId } = useFormField();
  const body = error ? String(error?.message ?? '') : props.children;

  if (!body) {
    return null;
  }

  return (
    <p
      data-slot="form-message"
      id={formMessageId}
      className={cn('text-destructive text-xs -mt-2', className)}
      {...props}
    >
      {body}
    </p>
  );
}

export {
  useFormField,
  Form,
  FormItem,
  FormLabel,
  FormControl,
  FormDescription,
  FormMessage,
  FormField,
};
</file>

<file path="src/components/ui/hover-card.tsx">
'use client';

import * as HoverCardPrimitive from '@radix-ui/react-hover-card';
import * as React from 'react';

import { cn } from '@/lib/utils';

function HoverCard({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
  return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
}

function HoverCardTrigger({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
  return <HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />;
}

function HoverCardContent({
  className,
  align = 'center',
  sideOffset = 4,
  ...props
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
  return (
    <HoverCardPrimitive.Content
      data-slot="hover-card-content"
      align={align}
      sideOffset={sideOffset}
      className={cn(
        'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 rounded-md border p-4 shadow-md outline-hidden',
        className,
      )}
      {...props}
    />
  );
}

export { HoverCard, HoverCardTrigger, HoverCardContent };
</file>

<file path="src/components/ui/input-otp.tsx">
'use client';

import { OTPInput, OTPInputContext } from 'input-otp';
import { MinusIcon } from 'lucide-react';
import * as React from 'react';

import { cn } from '@/lib/utils';

function InputOTP({
  className,
  containerClassName,
  ...props
}: React.ComponentProps<typeof OTPInput> & {
  containerClassName?: string;
}) {
  return (
    <OTPInput
      data-slot="input-otp"
      containerClassName={cn('flex items-center gap-2 has-disabled:opacity-70', containerClassName)}
      className={cn('disabled:cursor-not-allowed', className)}
      {...props}
    />
  );
}

function InputOTPGroup({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div data-slot="input-otp-group" className={cn('flex items-center', className)} {...props} />
  );
}

function InputOTPSlot({
  index,
  className,
  ...props
}: React.ComponentProps<'div'> & {
  index: number;
}) {
  const inputOTPContext = React.useContext(OTPInputContext);
  const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};

  return (
    <div
      data-slot="input-otp-slot"
      data-active={isActive}
      className={cn(
        'border-input data-[active=true]:border-ring data-[active=true]:ring-ring/15 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]',
        className,
      )}
      {...props}
    >
      {char}
      {hasFakeCaret && (
        <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
          <div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
        </div>
      )}
    </div>
  );
}

function InputOTPSeparator({ ...props }: React.ComponentProps<'div'>) {
  return (
    <div data-slot="input-otp-separator" role="separator" {...props}>
      <MinusIcon />
    </div>
  );
}

export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
</file>

<file path="src/components/ui/input.tsx">
import * as React from 'react';

import { cn } from '@/lib/utils';

function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
  return (
    <input
      type={type}
      data-slot="input"
      className={cn(
        'border-input file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground flex h-10 w-full min-w-0 rounded-md border bg-card px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-70 md:text-sm',
        'focus-visible:border-ring focus-visible:ring-ring/15 focus-visible:ring-[3px]',
        'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
        className,
      )}
      {...props}
    />
  );
}

export { Input };
</file>

<file path="src/components/ui/label.tsx">
'use client';

import * as LabelPrimitive from '@radix-ui/react-label';
import * as React from 'react';

import { cn } from '@/lib/utils';

function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
  return (
    <LabelPrimitive.Root
      data-slot="label"
      className={cn(
        'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
        className,
      )}
      {...props}
    />
  );
}

export { Label };
</file>

<file path="src/components/ui/menubar.tsx">
'use client';

import * as MenubarPrimitive from '@radix-ui/react-menubar';
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
import * as React from 'react';

import { cn } from '@/lib/utils';

function Menubar({ className, ...props }: React.ComponentProps<typeof MenubarPrimitive.Root>) {
  return (
    <MenubarPrimitive.Root
      data-slot="menubar"
      className={cn(
        'bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs',
        className,
      )}
      {...props}
    />
  );
}

function MenubarMenu({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
  return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />;
}

function MenubarGroup({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Group>) {
  return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />;
}

function MenubarPortal({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
  return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />;
}

function MenubarRadioGroup({ ...props }: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
  return <MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />;
}

function MenubarTrigger({
  className,
  ...props
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
  return (
    <MenubarPrimitive.Trigger
      data-slot="menubar-trigger"
      className={cn(
        'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none',
        className,
      )}
      {...props}
    />
  );
}

function MenubarContent({
  className,
  align = 'start',
  alignOffset = -4,
  sideOffset = 8,
  ...props
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
  return (
    <MenubarPortal>
      <MenubarPrimitive.Content
        data-slot="menubar-content"
        align={align}
        alignOffset={alignOffset}
        sideOffset={sideOffset}
        className={cn(
          'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] overflow-hidden rounded-md border p-1 shadow-md',
          className,
        )}
        {...props}
      />
    </MenubarPortal>
  );
}

function MenubarItem({
  className,
  inset,
  variant = 'default',
  ...props
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
  inset?: boolean;
  variant?: 'default' | 'destructive';
}) {
  return (
    <MenubarPrimitive.Item
      data-slot="menubar-item"
      data-inset={inset}
      data-variant={variant}
      className={cn(
        "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive-foreground data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/40 data-[variant=destructive]:focus:text-destructive-foreground data-[variant=destructive]:*:[svg]:!text-destructive-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className,
      )}
      {...props}
    />
  );
}

function MenubarCheckboxItem({
  className,
  children,
  checked,
  ...props
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
  return (
    <MenubarPrimitive.CheckboxItem
      data-slot="menubar-checkbox-item"
      className={cn(
        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className,
      )}
      checked={checked}
      {...props}
    >
      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
        <MenubarPrimitive.ItemIndicator>
          <CheckIcon className="size-4" />
        </MenubarPrimitive.ItemIndicator>
      </span>
      {children}
    </MenubarPrimitive.CheckboxItem>
  );
}

function MenubarRadioItem({
  className,
  children,
  ...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
  return (
    <MenubarPrimitive.RadioItem
      data-slot="menubar-radio-item"
      className={cn(
        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className,
      )}
      {...props}
    >
      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
        <MenubarPrimitive.ItemIndicator>
          <CircleIcon className="size-2 fill-current" />
        </MenubarPrimitive.ItemIndicator>
      </span>
      {children}
    </MenubarPrimitive.RadioItem>
  );
}

function MenubarLabel({
  className,
  inset,
  ...props
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
  inset?: boolean;
}) {
  return (
    <MenubarPrimitive.Label
      data-slot="menubar-label"
      data-inset={inset}
      className={cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', className)}
      {...props}
    />
  );
}

function MenubarSeparator({
  className,
  ...props
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
  return (
    <MenubarPrimitive.Separator
      data-slot="menubar-separator"
      className={cn('bg-border -mx-1 my-1 h-px', className)}
      {...props}
    />
  );
}

function MenubarShortcut({ className, ...props }: React.ComponentProps<'span'>) {
  return (
    <span
      data-slot="menubar-shortcut"
      className={cn('text-muted-foreground ml-auto text-xs tracking-widest', className)}
      {...props}
    />
  );
}

function MenubarSub({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
  return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />;
}

function MenubarSubTrigger({
  className,
  inset,
  children,
  ...props
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
  inset?: boolean;
}) {
  return (
    <MenubarPrimitive.SubTrigger
      data-slot="menubar-sub-trigger"
      data-inset={inset}
      className={cn(
        'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8',
        className,
      )}
      {...props}
    >
      {children}
      <ChevronRightIcon className="ml-auto h-4 w-4" />
    </MenubarPrimitive.SubTrigger>
  );
}

function MenubarSubContent({
  className,
  ...props
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
  return (
    <MenubarPrimitive.SubContent
      data-slot="menubar-sub-content"
      className={cn(
        'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg',
        className,
      )}
      {...props}
    />
  );
}

export {
  Menubar,
  MenubarPortal,
  MenubarMenu,
  MenubarTrigger,
  MenubarContent,
  MenubarGroup,
  MenubarSeparator,
  MenubarLabel,
  MenubarItem,
  MenubarShortcut,
  MenubarCheckboxItem,
  MenubarRadioGroup,
  MenubarRadioItem,
  MenubarSub,
  MenubarSubTrigger,
  MenubarSubContent,
};
</file>

<file path="src/components/ui/multi-select.tsx">
import { cva, type VariantProps } from 'class-variance-authority';
import { CheckIcon, XIcon, ChevronDown, WandSparkles } from 'lucide-react';
import * as React from 'react';

import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
} from '@/components/ui/command';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Separator } from '@/components/ui/separator';

import { cn } from '@/lib/utils';

/**
 * Variants for the multi-select component to handle different styles.
 * Uses class-variance-authority (cva) to define different styles based on "variant" prop.
 */
const multiSelectVariants = cva('m-1 transition', {
  variants: {
    variant: {
      default: 'border-foreground/10 text-foreground bg-card hover:bg-card/80',
      secondary:
        'border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80',
      destructive:
        'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
      inverted: 'inverted',
    },
  },
  defaultVariants: {
    variant: 'default',
  },
});

/**
 * Props for MultiSelect component
 */
interface MultiSelectProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof multiSelectVariants> {
  /**
   * An array of option objects to be displayed in the multi-select component.
   * Each option object has a label, value, and an optional icon.
   */
  options: {
    /** The text to display for the option. */
    label: string;
    /** The unique value associated with the option. */
    value: string;
    /** Optional icon component to display alongside the option. */
    icon?: React.ComponentType<{ className?: string }>;
  }[];

  /**
   * Callback function triggered when the selected values change.
   * Receives an array of the new selected values.
   */
  onValueChange: (value: string[]) => void;

  /** The default selected values when the component mounts. */
  value?: string[];

  /**
   * Placeholder text to be displayed when no values are selected.
   * Optional, defaults to "Select options".
   */
  placeholder?: string;

  /**
   * Animation duration in seconds for the visual effects (e.g., bouncing badges).
   * Optional, defaults to 0 (no animation).
   */
  animation?: number;

  /**
   * Maximum number of items to display. Extra selected items will be summarized.
   * Optional, defaults to 3.
   */
  maxCount?: number;

  /**
   * The modality of the popover. When set to true, interaction with outside elements
   * will be disabled and only popover content will be visible to screen readers.
   * Optional, defaults to false.
   */
  modalPopover?: boolean;

  /**
   * Additional class names to apply custom styles to the multi-select component.
   * Optional, can be used to add custom styles.
   */
  className?: string;

  optionsClassName?: string;
}

export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>(
  (
    {
      options,
      onValueChange,
      variant,
      placeholder = 'Select options',
      animation = 0,
      maxCount = 3,
      modalPopover = false,
      className,
      value = [],
      optionsClassName,
      ...props
    },
    ref,
  ) => {
    const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
    const [isAnimating, setIsAnimating] = React.useState(false);

    const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
      if (event.key === 'Enter') {
        setIsPopoverOpen(true);
      } else if (event.key === 'Backspace' && !event.currentTarget.value) {
        const newSelectedValues = [...value];
        newSelectedValues.pop();
        onValueChange(newSelectedValues);
      }
    };

    const toggleOption = (option: string) => {
      const newSelectedValues = value.includes(option)
        ? value.filter((value) => value !== option)
        : [...value, option];
      onValueChange(newSelectedValues);
    };

    const handleClear = () => {
      onValueChange([]);
    };

    const handleTogglePopover = () => {
      setIsPopoverOpen((prev) => !prev);
    };

    const clearExtraOptions = () => {
      const newSelectedValues = value.slice(0, maxCount);
      onValueChange(newSelectedValues);
    };

    const toggleAll = () => {
      if (value.length === options.length) {
        handleClear();
      } else {
        const allValues = options.map((option) => option.value);
        onValueChange(allValues);
      }
    };

    return (
      <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen} modal={modalPopover}>
        <PopoverTrigger asChild>
          <Button
            ref={ref}
            {...props}
            onClick={handleTogglePopover}
            className={cn(
              'flex w-full p-1 rounded-md !bg-card border min-h-10 h-auto items-center justify-between hover:bg-inherit [&_svg]:pointer-events-auto',
              className,
            )}
          >
            {value.length > 0 ? (
              <div className="flex justify-between items-center w-full">
                <div className="flex flex-wrap items-center">
                  {value.slice(0, maxCount).map((value) => {
                    const option = options.find((o) => o.value === value);
                    const IconComponent = option?.icon;

                    return (
                      <Badge key={value} className={cn(multiSelectVariants({ variant }), 'pr-1')}>
                        {IconComponent && <IconComponent className="h-4 w-4 mr-2" />}
                        {option?.label}
                        <div
                          className="flex items-center justify-center !size-4"
                          onClick={(e) => {
                            e.preventDefault();
                            e.stopPropagation();
                            toggleOption(value);
                          }}
                        >
                          <XIcon className="!size-3 cursor-pointer" />
                        </div>
                      </Badge>
                    );
                  })}
                  {value.length > maxCount && (
                    <Badge
                      className={cn(
                        'bg-transparent text-foreground border-foreground/1 hover:bg-transparent',
                        isAnimating ? 'animate-bounce' : '',
                        multiSelectVariants({ variant }),
                      )}
                      style={{ animationDuration: `${animation}s` }}
                    >
                      {`+ ${value.length - maxCount} more`}
                      <div
                        className="flex items-center justify-center !size-4"
                        onClick={(e) => {
                          e.preventDefault();
                          e.stopPropagation();
                          clearExtraOptions();
                        }}
                      >
                        <XIcon className="!size-3 cursor-pointer" />
                      </div>
                    </Badge>
                  )}
                </div>
                <div className="flex items-center justify-between">
                  <XIcon
                    className="h-4 mx-2 cursor-pointer text-muted-foreground"
                    onClick={(event) => {
                      event.stopPropagation();
                      handleClear();
                    }}
                  />
                  <Separator orientation="vertical" className="flex min-h-6 h-full" />
                  <ChevronDown className="h-4 mx-2 cursor-pointer text-muted-foreground" />
                </div>
              </div>
            ) : (
              <div className="flex items-center justify-between w-full mx-auto">
                <span className="text-sm text-muted-foreground mx-3">{placeholder}</span>
                <ChevronDown className="h-4 cursor-pointer text-muted-foreground mx-2" />
              </div>
            )}
          </Button>
        </PopoverTrigger>
        <PopoverContent
          className={cn('w-[var(--radix-popover-trigger-width)] p-0', optionsClassName)}
          align="start"
          onEscapeKeyDown={() => setIsPopoverOpen(false)}
        >
          <Command>
            <CommandInput
              className="!h-10"
              placeholder="Search..."
              onKeyDown={handleInputKeyDown}
            />
            <CommandList>
              <CommandEmpty>No results found.</CommandEmpty>
              <CommandGroup>
                <CommandItem key="all" onSelect={toggleAll} className="cursor-pointer">
                  <div
                    className={cn(
                      'flex  size-[17px] shrink-0 rounded-[5px] items-center justify-center border border-foreground',
                      value.length === options.length
                        ? 'bg-primary text-primary-foreground'
                        : 'opacity-50 [&_svg]:invisible',
                    )}
                  >
                    <CheckIcon className="size-3 text-primary-foreground" />
                  </div>
                  <span>(Select All)</span>
                </CommandItem>
                {options.map((option) => {
                  const isSelected = value.includes(option.value);

                  return (
                    <CommandItem
                      key={option.value}
                      onSelect={() => toggleOption(option.value)}
                      className="cursor-pointer"
                    >
                      <div
                        className={cn(
                          'flex size-[17px] shrink-0 rounded-[5px] items-center justify-center border border-foreground',
                          isSelected
                            ? 'bg-primary text-primary-foreground'
                            : 'opacity-50 [&_svg]:invisible',
                        )}
                      >
                        <CheckIcon className="size-3 text-primary-foreground" />
                      </div>
                      {option.icon && (
                        <option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
                      )}
                      <span>{option.label}</span>
                    </CommandItem>
                  );
                })}
              </CommandGroup>
              <CommandSeparator />
              <CommandGroup className="p-0">
                <div className="flex items-center justify-between">
                  {value.length > 0 && (
                    <>
                      <CommandItem
                        onSelect={handleClear}
                        className="flex-1 justify-center cursor-pointer p-2.5 rounded-none"
                      >
                        Clear
                      </CommandItem>
                      <Separator orientation="vertical" className="flex min-h-6 h-full" />
                    </>
                  )}
                  <CommandItem
                    onSelect={() => setIsPopoverOpen(false)}
                    className="flex-1 justify-center cursor-pointer max-w-full p-2.5 rounded-none"
                  >
                    Close
                  </CommandItem>
                </div>
              </CommandGroup>
            </CommandList>
          </Command>
        </PopoverContent>
        {animation > 0 && value.length > 0 && (
          <WandSparkles
            className={cn(
              'cursor-pointer my-2 text-foreground bg-background w-3 h-3',
              isAnimating ? '' : 'text-muted-foreground',
            )}
            onClick={() => setIsAnimating(!isAnimating)}
          />
        )}
      </Popover>
    );
  },
);

MultiSelect.displayName = 'MultiSelect';
</file>

<file path="src/components/ui/navigation-menu.tsx">
import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu';
import { cva } from 'class-variance-authority';
import { ChevronDown } from 'lucide-react';
import * as React from 'react';

import { cn } from '@/lib/utils';

const NavigationMenu = React.forwardRef<
  React.ComponentRef<typeof NavigationMenuPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
  <NavigationMenuPrimitive.Root
    ref={ref}
    className={cn('relative z-10 flex max-w-max flex-1 items-center justify-center', className)}
    {...props}
  >
    {children}
    <NavigationMenuViewport />
  </NavigationMenuPrimitive.Root>
));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;

const NavigationMenuList = React.forwardRef<
  React.ComponentRef<typeof NavigationMenuPrimitive.List>,
  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
  <NavigationMenuPrimitive.List
    ref={ref}
    className={cn('group flex flex-1 list-none items-center justify-center space-x-1', className)}
    {...props}
  />
));
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;

const NavigationMenuItem = NavigationMenuPrimitive.Item;

const navigationMenuTriggerStyle = cva(
  'group inline-flex h-9 w-max items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:text-accent-foreground data-[state=open]:bg-accent/50 data-[state=open]:hover:bg-accent data-[state=active]:bg-accent data-[state=open]:focus:bg-accent',
);

const NavigationMenuTrigger = React.forwardRef<
  React.ComponentRef<typeof NavigationMenuPrimitive.Trigger>,
  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
  <NavigationMenuPrimitive.Trigger
    ref={ref}
    className={cn(navigationMenuTriggerStyle(), 'group', className)}
    {...props}
  >
    {children}{' '}
    <ChevronDown
      className="relative top-[1px] ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180"
      aria-hidden="true"
    />
  </NavigationMenuPrimitive.Trigger>
));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;

const NavigationMenuContent = React.forwardRef<
  React.ComponentRef<typeof NavigationMenuPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
  <NavigationMenuPrimitive.Content
    ref={ref}
    className={cn(
      'left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-0 data-[motion=from-start]:slide-in-from-left-0 data-[motion=to-end]:slide-out-to-right-0 data-[motion=to-start]:slide-out-to-left-0 md:absolute md:w-auto ',
      className,
    )}
    {...props}
  />
));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;

const NavigationMenuLink = NavigationMenuPrimitive.Link;

const NavigationMenuViewport = React.forwardRef<
  React.ComponentRef<typeof NavigationMenuPrimitive.Viewport>,
  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
  <div className={cn('absolute left-0 top-full flex justify-center')}>
    <NavigationMenuPrimitive.Viewport
      className={cn(
        'origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]',
        className,
      )}
      ref={ref}
      {...props}
    />
  </div>
));
NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName;

const NavigationMenuIndicator = React.forwardRef<
  React.ComponentRef<typeof NavigationMenuPrimitive.Indicator>,
  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
  <NavigationMenuPrimitive.Indicator
    ref={ref}
    className={cn(
      'top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in',
      className,
    )}
    {...props}
  >
    <div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
  </NavigationMenuPrimitive.Indicator>
));
NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName;

export {
  navigationMenuTriggerStyle,
  NavigationMenu,
  NavigationMenuList,
  NavigationMenuItem,
  NavigationMenuContent,
  NavigationMenuTrigger,
  NavigationMenuLink,
  NavigationMenuIndicator,
  NavigationMenuViewport,
};
</file>

<file path="src/components/ui/number-input.tsx">
import { ChevronDown, ChevronUp } from 'lucide-react';
import { forwardRef, useCallback, useEffect, useState } from 'react';
import { NumericFormat, NumericFormatProps } from 'react-number-format';

import { Button } from './button';
import { Input } from './input';

export interface NumberInputProps extends Omit<NumericFormatProps, 'value' | 'onValueChange'> {
  stepper?: number;
  thousandSeparator?: string;
  placeholder?: string;
  defaultValue?: number;
  min?: number;
  max?: number;
  value?: number; // Controlled value
  suffix?: string;
  prefix?: string;
  onValueChange?: (value: number | undefined) => void;
  fixedDecimalScale?: boolean;
  decimalScale?: number;
}

export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
  (
    {
      stepper,
      thousandSeparator,
      placeholder,
      defaultValue,
      min = -Infinity,
      max = Infinity,
      onValueChange,
      fixedDecimalScale = false,
      decimalScale = 0,
      suffix,
      prefix,
      value: controlledValue,
      ...props
    },
    ref,
  ) => {
    const [value, setValue] = useState<number | undefined>(controlledValue ?? defaultValue);

    const handleIncrement = useCallback(() => {
      setValue((prev) =>
        prev === undefined ? (stepper ?? 1) : Math.min(prev + (stepper ?? 1), max),
      );
    }, [stepper, max]);

    const handleDecrement = useCallback(() => {
      setValue((prev) =>
        prev === undefined ? -(stepper ?? 1) : Math.max(prev - (stepper ?? 1), min),
      );
    }, [stepper, min]);

    useEffect(() => {
      const handleKeyDown = (e: KeyboardEvent) => {
        if (document.activeElement === (ref as React.RefObject<HTMLInputElement>)?.current) {
          if (e.key === 'ArrowUp') {
            handleIncrement();
          } else if (e.key === 'ArrowDown') {
            handleDecrement();
          }
        }
      };

      window.addEventListener('keydown', handleKeyDown);

      return () => {
        window.removeEventListener('keydown', handleKeyDown);
      };
    }, [handleIncrement, handleDecrement, ref]);

    useEffect(() => {
      if (controlledValue !== undefined) {
        setValue(controlledValue);
      }
    }, [controlledValue]);

    const handleChange = (values: { value: string; floatValue: number | undefined }) => {
      const newValue = values.floatValue === undefined ? undefined : values.floatValue;
      setValue(newValue);
      if (onValueChange) {
        onValueChange(newValue);
      }
    };

    const handleBlur = () => {
      if (value !== undefined) {
        if (value < min) {
          setValue(min);
          (ref as React.RefObject<HTMLInputElement>).current!.value = String(min);
        } else if (value > max) {
          setValue(max);
          (ref as React.RefObject<HTMLInputElement>).current!.value = String(max);
        }
      }
    };

    return (
      <div className="flex items-center">
        <NumericFormat
          value={value}
          onValueChange={handleChange}
          thousandSeparator={thousandSeparator}
          decimalScale={decimalScale}
          fixedDecimalScale={fixedDecimalScale}
          allowNegative={min < 0}
          valueIsNumericString
          onBlur={handleBlur}
          max={max}
          min={min}
          suffix={suffix}
          prefix={prefix}
          customInput={Input}
          placeholder={placeholder}
          className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none rounded-r-none relative"
          getInputRef={ref}
          {...props}
        />

        <div className="flex flex-col">
          <Button
            aria-label="Increase value"
            className="px-2 h-5 rounded-l-none rounded-br-none border-input border-l-0 border-b-[0.5px] focus-visible:relative"
            variant="outline"
            onClick={handleIncrement}
            disabled={value === max}
          >
            <ChevronUp size={15} />
          </Button>
          <Button
            aria-label="Decrease value"
            className="px-2 h-5 rounded-l-none rounded-tr-none border-input border-l-0 border-t-[0.5px] focus-visible:relative"
            variant="outline"
            onClick={handleDecrement}
            disabled={value === min}
          >
            <ChevronDown size={15} />
          </Button>
        </div>
      </div>
    );
  },
);
NumberInput.displayName = 'NumberInput';
</file>

<file path="src/components/ui/pagination.tsx">
import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from 'lucide-react';
import * as React from 'react';

import { Button, buttonVariants } from '@/components/ui/button';

import { cn } from '@/lib/utils';

function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
  return (
    <nav
      role="navigation"
      aria-label="pagination"
      data-slot="pagination"
      className={cn('mx-auto flex w-full justify-center', className)}
      {...props}
    />
  );
}

function PaginationContent({ className, ...props }: React.ComponentProps<'ul'>) {
  return (
    <ul
      data-slot="pagination-content"
      className={cn('flex flex-row items-center gap-1', className)}
      {...props}
    />
  );
}

function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
  return <li data-slot="pagination-item" {...props} />;
}

type PaginationLinkProps = {
  isActive?: boolean;
} & Pick<React.ComponentProps<typeof Button>, 'size'> &
  React.ComponentProps<'a'>;

function PaginationLink({ className, isActive, size = 'icon', ...props }: PaginationLinkProps) {
  return (
    <a
      aria-current={isActive ? 'page' : undefined}
      data-slot="pagination-link"
      data-active={isActive}
      className={cn(
        buttonVariants({
          variant: isActive ? 'outline' : 'ghost',
          size,
        }),
        className,
      )}
      {...props}
    />
  );
}

function PaginationPrevious({ className, ...props }: React.ComponentProps<typeof PaginationLink>) {
  return (
    <PaginationLink
      aria-label="Go to previous page"
      size="default"
      className={cn('gap-1 px-2.5 sm:pl-2.5', className)}
      {...props}
    >
      <ChevronLeftIcon />
      <span className="hidden sm:block">Previous</span>
    </PaginationLink>
  );
}

function PaginationNext({ className, ...props }: React.ComponentProps<typeof PaginationLink>) {
  return (
    <PaginationLink
      aria-label="Go to next page"
      size="default"
      className={cn('gap-1 px-2.5 sm:pr-2.5', className)}
      {...props}
    >
      <span className="hidden sm:block">Next</span>
      <ChevronRightIcon />
    </PaginationLink>
  );
}

function PaginationEllipsis({ className, ...props }: React.ComponentProps<'span'>) {
  return (
    <span
      aria-hidden
      data-slot="pagination-ellipsis"
      className={cn('flex size-9 items-center justify-center', className)}
      {...props}
    >
      <MoreHorizontalIcon className="size-4" />
      <span className="sr-only">More pages</span>
    </span>
  );
}

export {
  Pagination,
  PaginationContent,
  PaginationLink,
  PaginationItem,
  PaginationPrevious,
  PaginationNext,
  PaginationEllipsis,
};
</file>

<file path="src/components/ui/password-input.tsx">
'use client';

import { EyeIcon, EyeOffIcon } from 'lucide-react';
import * as React from 'react';

import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';

import { cn } from '@/lib/utils';

const PasswordInput = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
  ({ className, ...props }, ref) => {
    const [showPassword, setShowPassword] = React.useState(false);
    const disabled = props.value === '' || props.value === undefined || props.disabled;

    return (
      <div className="relative">
        <Input
          {...props}
          type={showPassword ? 'text' : 'password'}
          name="password_fake"
          className={cn('hide-password-toggle pr-10', className)}
          ref={ref}
        />
        <Button
          type="button"
          variant="ghost"
          size="sm"
          className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
          onClick={() => setShowPassword((prev) => !prev)}
          disabled={disabled}
        >
          {showPassword && !disabled ? (
            <EyeIcon className="size-4" aria-hidden="true" />
          ) : (
            <EyeOffIcon className="size-4" aria-hidden="true" />
          )}
          <span className="sr-only">{showPassword ? 'Hide password' : 'Show password'}</span>
        </Button>

        <style>{`
					.hide-password-toggle::-ms-reveal,
					.hide-password-toggle::-ms-clear {
						visibility: hidden;
						pointer-events: none;
						display: none;
					}
				`}</style>
      </div>
    );
  },
);
PasswordInput.displayName = 'PasswordInput';

export { PasswordInput };
</file>

<file path="src/components/ui/popover.tsx">
'use client';

import * as PopoverPrimitive from '@radix-ui/react-popover';
import * as React from 'react';

import { cn } from '@/lib/utils';

function Popover({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
  return <PopoverPrimitive.Root data-slot="popover" {...props} />;
}

function PopoverTrigger({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
  return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
}

function PopoverContent({
  className,
  align = 'center',
  sideOffset = 4,
  ...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
  return (
    <PopoverPrimitive.Portal>
      <PopoverPrimitive.Content
        data-slot="popover-content"
        align={align}
        sideOffset={sideOffset}
        className={cn(
          'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-lg border p-4 shadow-md outline-hidden',
          className,
        )}
        {...props}
      />
    </PopoverPrimitive.Portal>
  );
}

function PopoverAnchor({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
  return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
}

export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
</file>

<file path="src/components/ui/progress.tsx">
'use client';

import * as ProgressPrimitive from '@radix-ui/react-progress';
import * as React from 'react';

import { cn } from '@/lib/utils';

function Progress({
  className,
  value,
  ...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
  return (
    <ProgressPrimitive.Root
      data-slot="progress"
      className={cn('bg-primary/20 relative h-2 w-full overflow-hidden rounded-full', className)}
      {...props}
    >
      <ProgressPrimitive.Indicator
        data-slot="progress-indicator"
        className="bg-primary h-full w-full flex-1 transition-all"
        style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
      />
    </ProgressPrimitive.Root>
  );
}

export { Progress };
</file>

<file path="src/components/ui/radio-group.tsx">
'use client';

import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
import { CircleIcon } from 'lucide-react';
import * as React from 'react';

import { cn } from '@/lib/utils';

function RadioGroup({
  className,
  ...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
  return (
    <RadioGroupPrimitive.Root
      data-slot="radio-group"
      className={cn('grid gap-3', className)}
      {...props}
    />
  );
}

function RadioGroupItem({
  className,
  ...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
  return (
    <RadioGroupPrimitive.Item
      data-slot="radio-group-item"
      className={cn(
        'border-input text-primary focus-visible:border-ring focus-visible:ring-ring/15 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-70',
        className,
      )}
      {...props}
    >
      <RadioGroupPrimitive.Indicator
        data-slot="radio-group-indicator"
        className="relative flex items-center justify-center"
      >
        <CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
      </RadioGroupPrimitive.Indicator>
    </RadioGroupPrimitive.Item>
  );
}

export { RadioGroup, RadioGroupItem };
</file>

<file path="src/components/ui/resizable.tsx">
'use client';

import { GripVerticalIcon } from 'lucide-react';
import * as React from 'react';
import * as ResizablePrimitive from 'react-resizable-panels';

import { cn } from '@/lib/utils';

function ResizablePanelGroup({
  className,
  ...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
  return (
    <ResizablePrimitive.PanelGroup
      data-slot="resizable-panel-group"
      className={cn('flex h-full w-full data-[panel-group-direction=vertical]:flex-col', className)}
      {...props}
    />
  );
}

function ResizablePanel({ ...props }: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
  return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
}

function ResizableHandle({
  withHandle,
  className,
  ...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
  withHandle?: boolean;
}) {
  return (
    <ResizablePrimitive.PanelResizeHandle
      data-slot="resizable-handle"
      className={cn(
        'bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90',
        className,
      )}
      {...props}
    >
      {withHandle && (
        <div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
          <GripVerticalIcon className="size-2.5" />
        </div>
      )}
    </ResizablePrimitive.PanelResizeHandle>
  );
}

export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
</file>

<file path="src/components/ui/scroll-area.tsx">
'use client';

import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
import * as React from 'react';

import { cn } from '@/lib/utils';

function ScrollArea({
  className,
  children,
  ...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
  return (
    <ScrollAreaPrimitive.Root
      data-slot="scroll-area"
      className={cn('relative', className)}
      {...props}
    >
      <ScrollAreaPrimitive.Viewport
        data-slot="scroll-area-viewport"
        className="ring-ring/10 dark:ring-ring/15 dark:outline-ring/40 outline-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1"
      >
        {children}
      </ScrollAreaPrimitive.Viewport>
      <ScrollBar />
      <ScrollAreaPrimitive.Corner />
    </ScrollAreaPrimitive.Root>
  );
}

function ScrollBar({
  className,
  orientation = 'vertical',
  ...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
  return (
    <ScrollAreaPrimitive.ScrollAreaScrollbar
      data-slot="scroll-area-scrollbar"
      orientation={orientation}
      className={cn(
        'flex touch-none p-px transition-colors select-none',
        orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent',
        orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent',
        className,
      )}
      {...props}
    >
      <ScrollAreaPrimitive.ScrollAreaThumb
        data-slot="scroll-area-thumb"
        className="bg-border relative flex-1 rounded-full"
      />
    </ScrollAreaPrimitive.ScrollAreaScrollbar>
  );
}

export { ScrollArea, ScrollBar };
</file>

<file path="src/components/ui/select.tsx">
import * as React from 'react';

import { cn } from '@/lib/utils';

function Select({ className, ...props }: React.ComponentProps<'select'>) {
  return (
    <select
      data-slot="select"
      className={cn(
        'border-input file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground flex h-10 min-w-[100px] rounded-md border bg-card px-3 appearance-none py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-70 md:text-sm',
        'focus-visible:border-ring focus-visible:ring-ring/15 focus-visible:ring-[3px]',
        'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
        className,
      )}
      {...props}
    />
  );
}

export { Select };
</file>

<file path="src/components/ui/separator.tsx">
'use client';

import * as SeparatorPrimitive from '@radix-ui/react-separator';
import * as React from 'react';

import { cn } from '@/lib/utils';

function Separator({
  className,
  orientation = 'horizontal',
  decorative = true,
  ...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
  return (
    <SeparatorPrimitive.Root
      data-slot="separator-root"
      decorative={decorative}
      orientation={orientation}
      className={cn(
        'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
        className,
      )}
      {...props}
    />
  );
}

export { Separator };
</file>

<file path="src/components/ui/sheet.tsx">
'use client';

import * as SheetPrimitive from '@radix-ui/react-dialog';
import { XIcon } from 'lucide-react';
import * as React from 'react';

import { cn } from '@/lib/utils';

function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
  return <SheetPrimitive.Root data-slot="sheet" {...props} />;
}

function SheetTrigger({ ...props }: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
  return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
}

function SheetClose({ ...props }: React.ComponentProps<typeof SheetPrimitive.Close>) {
  return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
}

function SheetPortal({ ...props }: React.ComponentProps<typeof SheetPrimitive.Portal>) {
  return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
}

function SheetOverlay({
  className,
  ...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
  return (
    <SheetPrimitive.Overlay
      data-slot="sheet-overlay"
      className={cn(
        'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
        className,
      )}
      {...props}
    />
  );
}

function SheetContent({
  className,
  children,
  side = 'right',
  ...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
  side?: 'top' | 'right' | 'bottom' | 'left';
}) {
  return (
    <SheetPortal>
      <SheetOverlay />
      <SheetPrimitive.Content
        data-slot="sheet-content"
        className={cn(
          'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
          side === 'right' &&
            'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
          side === 'left' &&
            'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
          side === 'top' &&
            'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
          side === 'bottom' &&
            'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
          className,
        )}
        {...props}
      >
        {children}
        <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
          <XIcon className="size-4" />
          <span className="sr-only">Close</span>
        </SheetPrimitive.Close>
      </SheetPrimitive.Content>
    </SheetPortal>
  );
}

function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot="sheet-header"
      className={cn('flex flex-col gap-1.5 p-4', className)}
      {...props}
    />
  );
}

function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot="sheet-footer"
      className={cn('mt-auto flex flex-col gap-2 p-4', className)}
      {...props}
    />
  );
}

function SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) {
  return (
    <SheetPrimitive.Title
      data-slot="sheet-title"
      className={cn('text-foreground font-semibold', className)}
      {...props}
    />
  );
}

function SheetDescription({
  className,
  ...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
  return (
    <SheetPrimitive.Description
      data-slot="sheet-description"
      className={cn('text-muted-foreground text-sm', className)}
      {...props}
    />
  );
}

export {
  Sheet,
  SheetTrigger,
  SheetClose,
  SheetContent,
  SheetHeader,
  SheetFooter,
  SheetTitle,
  SheetDescription,
};
</file>

<file path="src/components/ui/sidebar.tsx">
'use client';

import { useIsMobile } from '@/hooks/use-mobile';
import { Slot } from '@radix-ui/react-slot';
import { VariantProps, cva } from 'class-variance-authority';
import { PanelLeftIcon } from 'lucide-react';
import * as React from 'react';

import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
} from '@/components/ui/sheet';
import { Skeleton } from '@/components/ui/skeleton';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';

import { cn } from '@/lib/utils';

const SIDEBAR_COOKIE_NAME = 'sidebar_state';
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = '16rem';
const SIDEBAR_WIDTH_MOBILE = '18rem';
const SIDEBAR_WIDTH_ICON = '3rem';
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';

type SidebarContext = {
  state: 'expanded' | 'collapsed';
  open: boolean;
  setOpen: (open: boolean) => void;
  openMobile: boolean;
  setOpenMobile: (open: boolean) => void;
  isMobile: boolean;
  toggleSidebar: () => void;
};

const SidebarContext = React.createContext<SidebarContext | null>(null);

function useSidebar() {
  const context = React.useContext(SidebarContext);
  if (!context) {
    throw new Error('useSidebar must be used within a SidebarProvider.');
  }

  return context;
}

function SidebarProvider({
  defaultOpen = true,
  open: openProp,
  onOpenChange: setOpenProp,
  className,
  style,
  children,
  ...props
}: React.ComponentProps<'div'> & {
  defaultOpen?: boolean;
  open?: boolean;
  onOpenChange?: (open: boolean) => void;
}) {
  const isMobile = useIsMobile();
  const [openMobile, setOpenMobile] = React.useState(false);

  // This is the internal state of the sidebar.
  // We use openProp and setOpenProp for control from outside the component.
  const [_open, _setOpen] = React.useState(defaultOpen);
  const open = openProp ?? _open;
  const setOpen = React.useCallback(
    (value: boolean | ((value: boolean) => boolean)) => {
      const openState = typeof value === 'function' ? value(open) : value;
      if (setOpenProp) {
        setOpenProp(openState);
      } else {
        _setOpen(openState);
      }

      // This sets the cookie to keep the sidebar state.
      document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
    },
    [setOpenProp, open],
  );

  // Helper to toggle the sidebar.
  const toggleSidebar = React.useCallback(() => {
    return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
  }, [isMobile, setOpen, setOpenMobile]);

  // Adds a keyboard shortcut to toggle the sidebar.
  React.useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
        event.preventDefault();
        toggleSidebar();
      }
    };

    window.addEventListener('keydown', handleKeyDown);

    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [toggleSidebar]);

  // We add a state so that we can do data-state="expanded" or "collapsed".
  // This makes it easier to style the sidebar with Tailwind classes.
  const state = open ? 'expanded' : 'collapsed';

  const contextValue = React.useMemo<SidebarContext>(
    () => ({
      state,
      open,
      setOpen,
      isMobile,
      openMobile,
      setOpenMobile,
      toggleSidebar,
    }),
    [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
  );

  return (
    <SidebarContext.Provider value={contextValue}>
      <div
        data-slot="sidebar-wrapper"
        style={
          {
            '--sidebar-width': SIDEBAR_WIDTH,
            '--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
            ...style,
          } as React.CSSProperties
        }
        className={cn(
          'group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full',
          className,
        )}
        {...props}
      >
        {children}
      </div>
    </SidebarContext.Provider>
  );
}

function Sidebar({
  side = 'left',
  variant = 'sidebar',
  collapsible = 'offcanvas',
  className,
  children,
  ...props
}: React.ComponentProps<'div'> & {
  side?: 'left' | 'right';
  variant?: 'sidebar' | 'floating' | 'inset';
  collapsible?: 'offcanvas' | 'icon' | 'none';
}) {
  const { isMobile, state, openMobile, setOpenMobile } = useSidebar();

  if (collapsible === 'none') {
    return (
      <div
        data-slot="sidebar"
        className={cn(
          'bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col',
          className,
        )}
        {...props}
      >
        {children}
      </div>
    );
  }

  if (isMobile) {
    return (
      <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
        <SheetContent
          data-sidebar="sidebar"
          data-slot="sidebar"
          data-mobile="true"
          className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
          style={
            {
              '--sidebar-width': SIDEBAR_WIDTH_MOBILE,
            } as React.CSSProperties
          }
          side={side}
        >
          <SheetHeader className="sr-only">
            <SheetTitle>Sidebar</SheetTitle>
            <SheetDescription>Displays the mobile sidebar.</SheetDescription>
          </SheetHeader>
          <div className="flex h-full w-full flex-col">{children}</div>
        </SheetContent>
      </Sheet>
    );
  }

  return (
    <div
      className="group peer text-sidebar-foreground hidden md:block"
      data-state={state}
      data-collapsible={state === 'collapsed' ? collapsible : ''}
      data-variant={variant}
      data-side={side}
      data-slot="sidebar"
    >
      {/* This is what handles the sidebar gap on desktop */}
      <div
        className={cn(
          'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-snappy',
          'group-data-[collapsible=offcanvas]:w-0',
          'group-data-[side=right]:rotate-180',
          variant === 'floating' || variant === 'inset'
            ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
            : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
        )}
      />
      <div
        className={cn(
          'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-snappy md:flex',
          side === 'left'
            ? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
            : 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
          // Adjust the padding for floating and inset variants.
          variant === 'floating' || variant === 'inset'
            ? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
            : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l',
          className,
        )}
        {...props}
      >
        <div
          data-sidebar="sidebar"
          className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
        >
          {children}
        </div>
      </div>
    </div>
  );
}

function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<typeof Button>) {
  const { toggleSidebar } = useSidebar();

  return (
    <Button
      data-sidebar="trigger"
      data-slot="sidebar-trigger"
      variant="ghost"
      size="icon"
      className={cn('h-7 w-7', className)}
      onClick={(event) => {
        onClick?.(event);
        toggleSidebar();
      }}
      {...props}
    >
      <PanelLeftIcon />
      <span className="sr-only">Toggle Sidebar</span>
    </Button>
  );
}

function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) {
  const { toggleSidebar } = useSidebar();

  return (
    <button
      data-sidebar="rail"
      data-slot="sidebar-rail"
      aria-label="Toggle Sidebar"
      tabIndex={-1}
      onClick={toggleSidebar}
      title="Toggle Sidebar"
      className={cn(
        'hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-snappy group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex',
        'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize',
        '[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
        'hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full',
        '[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
        '[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
        className,
      )}
      {...props}
    />
  );
}

function SidebarInset({ className, ...props }: React.ComponentProps<'main'>) {
  return (
    <main
      data-slot="sidebar-inset"
      className={cn(
        'bg-background relative flex overflow-x-hidden flex-1 flex-col',
        'md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2',
        className,
      )}
      {...props}
    />
  );
}

function SidebarInput({ className, ...props }: React.ComponentProps<typeof Input>) {
  return (
    <Input
      data-slot="sidebar-input"
      data-sidebar="input"
      className={cn('bg-background h-8 w-full shadow-none', className)}
      {...props}
    />
  );
}

function SidebarHeader({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot="sidebar-header"
      data-sidebar="header"
      className={cn('flex flex-col gap-2 p-2', className)}
      {...props}
    />
  );
}

function SidebarFooter({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot="sidebar-footer"
      data-sidebar="footer"
      className={cn('flex flex-col gap-2 p-2', className)}
      {...props}
    />
  );
}

function SidebarSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) {
  return (
    <Separator
      data-slot="sidebar-separator"
      data-sidebar="separator"
      className={cn('bg-sidebar-border mx-2 w-auto', className)}
      {...props}
    />
  );
}

function SidebarContent({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot="sidebar-content"
      data-sidebar="content"
      className={cn(
        'flex min-h-0 flex-1 flex-col overflow-auto group-data-[collapsible=icon]:overflow-hidden',
        className,
      )}
      {...props}
    />
  );
}

function SidebarGroup({ className, ...props }: React.ComponentProps<'div'>) {
  const { open, isMobile } = useSidebar();

  return (
    <div
      data-slot="sidebar-group"
      data-sidebar="group"
      className={cn(
        'relative flex w-full min-w-0 flex-col p-3 transition-all',
        {
          'p-2': !isMobile && !open,
        },
        className,
      )}
      {...props}
    />
  );
}

function SidebarGroupLabel({
  className,
  asChild = false,
  ...props
}: React.ComponentProps<'div'> & { asChild?: boolean }) {
  const Comp = asChild ? Slot : 'div';

  return (
    <Comp
      data-slot="sidebar-group-label"
      data-sidebar="group-label"
      className={cn(
        'text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-snappy focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
        'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
        className,
      )}
      {...props}
    />
  );
}

function SidebarGroupAction({
  className,
  asChild = false,
  ...props
}: React.ComponentProps<'button'> & { asChild?: boolean }) {
  const Comp = asChild ? Slot : 'button';

  return (
    <Comp
      data-slot="sidebar-group-action"
      data-sidebar="group-action"
      className={cn(
        'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
        // Increases the hit area of the button on mobile.
        'after:absolute after:-inset-2 md:after:hidden',
        'group-data-[collapsible=icon]:hidden',
        className,
      )}
      {...props}
    />
  );
}

function SidebarGroupContent({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot="sidebar-group-content"
      data-sidebar="group-content"
      className={cn('w-full text-sm', className)}
      {...props}
    />
  );
}

function SidebarMenu({ className, ...props }: React.ComponentProps<'ul'>) {
  return (
    <ul
      data-slot="sidebar-menu"
      data-sidebar="menu"
      className={cn('flex w-full min-w-0 flex-col gap-1', className)}
      {...props}
    />
  );
}

function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) {
  return (
    <li
      data-slot="sidebar-menu-item"
      data-sidebar="menu-item"
      className={cn('group/menu-item relative', className)}
      {...props}
    />
  );
}

const sidebarMenuButtonVariants = cva(
  'peer/menu-button flex w-full items-center gap-2 cursor-pointer overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-70 data-[active=true]:font-medium group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-70 data-[active=true]:bg-primary/5 transition-all data-[active=true]:text-primary data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
  {
    variants: {
      variant: {
        default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
        outline:
          'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',
      },
      size: {
        default: 'h-8 text-sm',
        sm: 'h-7 text-xs',
        lg: 'h-12 text-sm group-data-[collapsible=icon]:p-0!',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
    },
  },
);

function SidebarMenuButton({
  asChild = false,
  isActive = false,
  variant = 'default',
  size = 'default',
  tooltip,
  className,
  ...props
}: React.ComponentProps<'button'> & {
  asChild?: boolean;
  isActive?: boolean;
  tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants>) {
  const Comp = asChild ? Slot : 'button';
  const { isMobile, state } = useSidebar();

  const button = (
    <Comp
      data-slot="sidebar-menu-button"
      data-sidebar="menu-button"
      data-size={size}
      data-active={isActive}
      className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
      {...props}
    />
  );

  if (!tooltip) {
    return button;
  }

  if (typeof tooltip === 'string') {
    tooltip = {
      children: tooltip,
    };
  }

  return (
    <Tooltip>
      <TooltipTrigger asChild>{button}</TooltipTrigger>
      <TooltipContent
        side="right"
        align="center"
        hidden={state !== 'collapsed' || isMobile}
        {...tooltip}
      />
    </Tooltip>
  );
}

function SidebarMenuAction({
  className,
  asChild = false,
  showOnHover = false,
  ...props
}: React.ComponentProps<'button'> & {
  asChild?: boolean;
  showOnHover?: boolean;
}) {
  const Comp = asChild ? Slot : 'button';

  return (
    <Comp
      data-slot="sidebar-menu-action"
      data-sidebar="menu-action"
      className={cn(
        'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
        // Increases the hit area of the button on mobile.
        'after:absolute after:-inset-2 md:after:hidden',
        'peer-data-[size=sm]/menu-button:top-1',
        'peer-data-[size=default]/menu-button:top-1.5',
        'peer-data-[size=lg]/menu-button:top-2.5',
        'group-data-[collapsible=icon]:hidden',
        showOnHover &&
          'peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0',
        className,
      )}
      {...props}
    />
  );
}

function SidebarMenuBadge({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot="sidebar-menu-badge"
      data-sidebar="menu-badge"
      className={cn(
        'text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none',
        'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
        'peer-data-[size=sm]/menu-button:top-1',
        'peer-data-[size=default]/menu-button:top-1.5',
        'peer-data-[size=lg]/menu-button:top-2.5',
        'group-data-[collapsible=icon]:hidden',
        className,
      )}
      {...props}
    />
  );
}

function SidebarMenuSkeleton({
  className,
  showIcon = false,
  ...props
}: React.ComponentProps<'div'> & {
  showIcon?: boolean;
}) {
  // Random width between 50 to 90%.
  const width = React.useMemo(() => {
    return `${Math.floor(Math.random() * 40) + 50}%`;
  }, []);

  return (
    <div
      data-slot="sidebar-menu-skeleton"
      data-sidebar="menu-skeleton"
      className={cn('flex h-8 items-center gap-2 rounded-md px-2', className)}
      {...props}
    >
      {showIcon && <Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />}
      <Skeleton
        className="h-4 max-w-(--skeleton-width) flex-1"
        data-sidebar="menu-skeleton-text"
        style={
          {
            '--skeleton-width': width,
          } as React.CSSProperties
        }
      />
    </div>
  );
}

function SidebarMenuSub({ className, ...props }: React.ComponentProps<'ul'>) {
  return (
    <ul
      data-slot="sidebar-menu-sub"
      data-sidebar="menu-sub"
      className={cn(
        'border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5',
        'group-data-[collapsible=icon]:hidden',
        className,
      )}
      {...props}
    />
  );
}

function SidebarMenuSubItem({ className, ...props }: React.ComponentProps<'li'>) {
  return (
    <li
      data-slot="sidebar-menu-sub-item"
      data-sidebar="menu-sub-item"
      className={cn('group/menu-sub-item relative', className)}
      {...props}
    />
  );
}

function SidebarMenuSubButton({
  asChild = false,
  size = 'md',
  isActive = false,
  className,
  ...props
}: React.ComponentProps<'a'> & {
  asChild?: boolean;
  size?: 'sm' | 'md';
  isActive?: boolean;
}) {
  const Comp = asChild ? Slot : 'a';

  return (
    <Comp
      data-slot="sidebar-menu-sub-button"
      data-sidebar="menu-sub-button"
      data-size={size}
      data-active={isActive}
      className={cn(
        'text-sidebar-foreground transition-all ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-sm px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-70 aria-disabled:pointer-events-none aria-disabled:opacity-70 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
        'data-[active=true]:bg-primary/5 data-[active=true]:font-medium data-[active=true]:text-primary',
        size === 'sm' && 'text-xs',
        size === 'md' && 'text-sm',
        'group-data-[collapsible=icon]:hidden',
        className,
      )}
      {...props}
    />
  );
}

export {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupAction,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInput,
  SidebarInset,
  SidebarMenu,
  SidebarMenuAction,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSkeleton,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarProvider,
  SidebarRail,
  SidebarSeparator,
  SidebarTrigger,
  useSidebar,
};
</file>

<file path="src/components/ui/skeleton.tsx">
import { cn } from '@/lib/utils';

function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot="skeleton"
      className={cn('bg-foreground/10 animate-pulse rounded-md duration-[1.5s]', className)}
      {...props}
    />
  );
}

export { Skeleton };
</file>

<file path="src/components/ui/slider.tsx">
'use client';

import * as SliderPrimitive from '@radix-ui/react-slider';
import * as React from 'react';

import { cn } from '@/lib/utils';

function Slider({
  className,
  defaultValue,
  value,
  min = 0,
  max = 100,
  ...props
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
  const _values = React.useMemo(
    () => (Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue : [min, max]),
    [value, defaultValue, min, max],
  );

  return (
    <SliderPrimitive.Root
      data-slot="slider"
      defaultValue={defaultValue}
      value={value}
      min={min}
      max={max}
      className={cn(
        'relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col',
        className,
      )}
      {...props}
    >
      <SliderPrimitive.Track
        data-slot="slider-track"
        className={cn(
          'bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5',
        )}
      >
        <SliderPrimitive.Range
          data-slot="slider-range"
          className={cn(
            'bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full',
          )}
        />
      </SliderPrimitive.Track>
      {Array.from({ length: _values.length }, (_, index) => (
        <SliderPrimitive.Thumb
          data-slot="slider-thumb"
          key={index}
          className="border-primary bg-background ring-ring/15 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-70"
        />
      ))}
    </SliderPrimitive.Root>
  );
}

export { Slider };
</file>

<file path="src/components/ui/sonner.tsx">
'use client';

import { useTheme } from 'next-themes';
import { Toaster as Sonner, ToasterProps } from 'sonner';

const Toaster = ({ ...props }: ToasterProps) => {
  const { theme = 'system' } = useTheme();

  return (
    <Sonner
      theme={theme as ToasterProps['theme']}
      className="toaster group"
      toastOptions={{
        classNames: {
          toast: '!bg-background !border-input !rounded-2xl !pr-12',
          closeButton: '!right-3 !transform-none !left-auto !top-1/2 !-translate-y-1/2',
        },
      }}
      closeButton
      richColors
      position="bottom-right"
      duration={4000}
      {...props}
    />
  );
};

export { Toaster };
</file>

<file path="src/components/ui/switch.tsx">
'use client';

import * as SwitchPrimitive from '@radix-ui/react-switch';
import * as React from 'react';

import { cn } from '@/lib/utils';

function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
  return (
    <SwitchPrimitive.Root
      data-slot="switch"
      className={cn(
        'peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/15 inline-flex h-5 w-9 shrink-0 items-center rounded-full border-2 border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-70',
        className,
      )}
      {...props}
    >
      <SwitchPrimitive.Thumb
        data-slot="switch-thumb"
        className={cn(
          'bg-background pointer-events-none block size-4 rounded-full ring-0 shadow-lg transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0',
        )}
      />
    </SwitchPrimitive.Root>
  );
}

export { Switch };
</file>

<file path="src/components/ui/table.tsx">
'use client';

import * as React from 'react';

import { cn } from '@/lib/utils';

function Table({ className, ...props }: React.ComponentProps<'table'>) {
  return (
    <table
      data-slot="table"
      className={cn('w-full caption-bottom text-sm', className)}
      {...props}
    />
  );
}

function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
  return (
    <thead
      data-slot="table-header"
      className={cn('[&_tr]:border-b bg-muted', className)}
      {...props}
    />
  );
}

function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
  return (
    <tbody
      data-slot="table-body"
      className={cn('[&_tr:last-child]:border-0 ', className)}
      {...props}
    />
  );
}

function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
  return (
    <tfoot
      data-slot="table-footer"
      className={cn('bg-muted/50 border-t font-medium [&>tr]:last:border-b-0', className)}
      {...props}
    />
  );
}

function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
  return (
    <tr
      data-slot="table-row"
      className={cn(
        'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
        className,
      )}
      {...props}
    />
  );
}

function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
  return (
    <th
      data-slot="table-head"
      className={cn(
        'text-muted-foreground h-10 px-3 text-left align-middle font-medium truncate [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
        className,
      )}
      {...props}
    />
  );
}

function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
  return (
    <td
      data-slot="table-cell"
      className={cn(
        'px-3 py-2.5 align-middle truncate [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
        className,
      )}
      {...props}
    />
  );
}

function TableCaption({ className, ...props }: React.ComponentProps<'caption'>) {
  return (
    <caption
      data-slot="table-caption"
      className={cn('text-muted-foreground mt-4 text-sm', className)}
      {...props}
    />
  );
}

export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
</file>

<file path="src/components/ui/tabs.tsx">
'use client';

import * as TabsPrimitive from '@radix-ui/react-tabs';
import * as React from 'react';

import { cn } from '@/lib/utils';

function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) {
  return (
    <TabsPrimitive.Root
      data-slot="tabs"
      className={cn('flex flex-col gap-2', className)}
      {...props}
    />
  );
}

function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>) {
  return (
    <TabsPrimitive.List
      data-slot="tabs-list"
      className={cn(
        'bg-muted text-muted-foreground inline-flex h-10 w-fit items-center justify-center rounded-lg p-1',
        className,
      )}
      {...props}
    />
  );
}

function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
  return (
    <TabsPrimitive.Trigger
      data-slot="tabs-trigger"
      className={cn(
        "data-[state=active]:bg-background cursor-pointer data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/15 focus-visible:outline-ring inline-flex flex-1 items-center justify-center gap-1.5 rounded-md px-4 py-1.5 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-70 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className,
      )}
      {...props}
    />
  );
}

function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
  return (
    <TabsPrimitive.Content
      data-slot="tabs-content"
      className={cn('flex-1 outline-none', className)}
      {...props}
    />
  );
}

export { Tabs, TabsList, TabsTrigger, TabsContent };
</file>

<file path="src/components/ui/tags-input.tsx">
'use client';

import { XIcon } from 'lucide-react';
import * as React from 'react';

import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';

import { cn } from '@/lib/utils';

type TagsInputProps = Omit<React.ComponentProps<'input'>, 'value' | 'onChange'> & {
  value?: string[];
  onChange: React.Dispatch<React.SetStateAction<string[]>>;
};

const TagsInput = React.forwardRef<HTMLInputElement, TagsInputProps>(
  ({ className, value = [], onChange, ...props }, ref) => {
    const [pendingDataPoint, setPendingDataPoint] = React.useState('');

    React.useEffect(() => {
      if (pendingDataPoint.includes(',')) {
        const newDataPoints = new Set([
          ...value,
          ...pendingDataPoint.split(',').map((chunk) => chunk.trim()),
        ]);
        onChange(Array.from(newDataPoints));
        setPendingDataPoint('');
      }
    }, [pendingDataPoint, onChange, value]);

    const addPendingDataPoint = () => {
      if (pendingDataPoint) {
        const newDataPoints = new Set([...value, pendingDataPoint]);
        onChange(Array.from(newDataPoints));
        setPendingDataPoint('');
      }
    };

    return (
      <div
        className={cn(
          'has-[:focus-visible]:outline-none has-[:focus-visible]:border-ring transition-all has-[:focus-visible]:ring-ring/15 has-[:focus-visible]:ring-[3px] min-h-10 flex w-full flex-wrap gap-2 rounded-md border border-input bg-card shadow-xs px-3 py-2 text-sm disabled:cursor-not-allowed disabled:opacity-50',
          className,
        )}
      >
        {value.map((item) => (
          <Badge key={item} variant="secondary" className="pr-1.5">
            {item}
            <Button
              variant="ghost"
              size="icon"
              className="ml-2 h-3 w-3"
              onClick={() => {
                onChange(value.filter((i) => i !== item));
              }}
            >
              <XIcon className="!w-3" />
            </Button>
          </Badge>
        ))}
        <input
          className="flex-1 outline-none placeholder:text-neutral-500 dark:placeholder:text-neutral-400"
          value={pendingDataPoint}
          onChange={(e) => setPendingDataPoint(e.target.value)}
          onKeyDown={(e) => {
            if (e.key === 'Enter' || e.key === ',') {
              e.preventDefault();
              addPendingDataPoint();
            } else if (e.key === 'Backspace' && pendingDataPoint.length === 0 && value.length > 0) {
              e.preventDefault();
              onChange(value.slice(0, -1));
            }
          }}
          onBlur={() => addPendingDataPoint()}
          {...props}
          ref={ref}
        />
      </div>
    );
  },
);

TagsInput.displayName = 'TagsInput';

export { TagsInput };
</file>

<file path="src/components/ui/testimonial-card.tsx">
'use client';

import { Star } from 'lucide-react';
import * as React from 'react';

import { AvatarWrapper, AvatarFallback, AvatarImage } from '@/components/ui/avatar';

import { cn } from '@/lib/utils';

export interface TestimonialProps extends React.HTMLAttributes<HTMLDivElement> {
  name: string;
  role: string;
  testimonial: string;
  rating?: number;
  image?: string;
}

const Testimonial = React.forwardRef<HTMLDivElement, TestimonialProps>(
  ({ name, role, testimonial, rating = 5, image, className, ...props }, ref) => {
    return (
      <div
        ref={ref}
        className={cn(
          'relative overflow-hidden rounded-2xl border border-primary/10 bg-background p-6 transition-all hover:shadow-lg dark:hover:shadow-primary/5 md:p-8',
          className,
        )}
        {...props}
      >
        <div className="absolute right-6 top-6 text-6xl font-serif text-muted-foreground/20">
          &quot;
        </div>

        <div className="flex flex-col gap-4 justify-between h-full">
          {rating > 0 && (
            <div className="flex gap-1">
              {Array.from({ length: 5 }).map((_, index) => (
                <Star
                  key={index}
                  size={16}
                  className={cn(
                    index < rating ? 'fill-yellow-400 text-yellow-400' : 'fill-muted text-muted',
                  )}
                />
              ))}
            </div>
          )}

          <p className="text-pretty text-base text-muted-foreground">{testimonial}</p>

          <div className="flex items-center gap-4 justify-start">
            <div className="flex items-center gap-4">
              {image && (
                <AvatarWrapper>
                  <AvatarImage src={image} alt={name} height={48} width={48} />
                  <AvatarFallback>{name[0]}</AvatarFallback>
                </AvatarWrapper>
              )}

              <div className="flex flex-col">
                <h3 className="font-semibold text-foreground">{name}</h3>
                <p className="text-sm text-muted-foreground">{role}</p>
              </div>
            </div>
          </div>
        </div>
      </div>
    );
  },
);
Testimonial.displayName = 'Testimonial';

export { Testimonial };
</file>

<file path="src/components/ui/textarea.tsx">
import * as React from 'react';

import { cn } from '@/lib/utils';

function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
  return (
    <textarea
      data-slot="textarea"
      className={cn(
        'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/15 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex field-sizing-content min-h-16 w-full rounded-md border bg-card px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-70 md:text-sm',
        className,
      )}
      {...props}
    />
  );
}

export { Textarea };
</file>

<file path="src/components/ui/time-format.tsx">
'use client';

import React from 'react';
import Moment from 'react-moment';

const TimeFormat = ({ time }: { time: string | Date }) => {
  return <Moment format="MMM DD, YYYY">{time}</Moment>;
};

export default TimeFormat;
</file>

<file path="src/components/ui/toggle-group.tsx">
'use client';

import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';
import { type VariantProps } from 'class-variance-authority';
import * as React from 'react';

import { toggleVariants } from '@/components/ui/toggle';

import { cn } from '@/lib/utils';

const ToggleGroupContext = React.createContext<VariantProps<typeof toggleVariants>>({
  size: 'default',
  variant: 'default',
});

function ToggleGroup({
  className,
  variant,
  size,
  children,
  ...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> & VariantProps<typeof toggleVariants>) {
  return (
    <ToggleGroupPrimitive.Root
      data-slot="toggle-group"
      data-variant={variant}
      data-size={size}
      className={cn(
        'group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs',
        className,
      )}
      {...props}
    >
      <ToggleGroupContext.Provider value={{ variant, size }}>
        {children}
      </ToggleGroupContext.Provider>
    </ToggleGroupPrimitive.Root>
  );
}

function ToggleGroupItem({
  className,
  children,
  variant,
  size,
  ...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> & VariantProps<typeof toggleVariants>) {
  const context = React.useContext(ToggleGroupContext);

  return (
    <ToggleGroupPrimitive.Item
      data-slot="toggle-group-item"
      data-variant={context.variant || variant}
      data-size={context.size || size}
      className={cn(
        toggleVariants({
          variant: context.variant || variant,
          size: context.size || size,
        }),
        'min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l',
        className,
      )}
      {...props}
    >
      {children}
    </ToggleGroupPrimitive.Item>
  );
}

export { ToggleGroup, ToggleGroupItem };
</file>

<file path="src/components/ui/toggle.tsx">
'use client';

import * as TogglePrimitive from '@radix-ui/react-toggle';
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';

import { cn } from '@/lib/utils';

const toggleVariants = cva(
  "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-70 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/15 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
  {
    variants: {
      variant: {
        default: 'bg-transparent',
        outline:
          'border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground',
      },
      size: {
        default: 'h-9 px-2 min-w-9',
        sm: 'h-8 px-1.5 min-w-8',
        lg: 'h-10 px-2.5 min-w-10',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
    },
  },
);

function Toggle({
  className,
  variant,
  size,
  ...props
}: React.ComponentProps<typeof TogglePrimitive.Root> & VariantProps<typeof toggleVariants>) {
  return (
    <TogglePrimitive.Root
      data-slot="toggle"
      className={cn(toggleVariants({ variant, size, className }))}
      {...props}
    />
  );
}

export { Toggle, toggleVariants };
</file>

<file path="src/components/ui/tooltip.tsx">
'use client';

import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import * as React from 'react';

import { cn } from '@/lib/utils';

function TooltipProvider({
  delayDuration = 0,
  ...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
  return (
    <TooltipPrimitive.Provider
      data-slot="tooltip-provider"
      delayDuration={delayDuration}
      {...props}
    />
  );
}

function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
  return (
    <TooltipProvider>
      <TooltipPrimitive.Root data-slot="tooltip" {...props} />
    </TooltipProvider>
  );
}

function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
  return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}

function TooltipContent({
  className,
  sideOffset = 0,
  children,
  ...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
  return (
    <TooltipPrimitive.Portal>
      <TooltipPrimitive.Content
        data-slot="tooltip-content"
        sideOffset={sideOffset}
        className={cn(
          'bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance',
          className,
        )}
        {...props}
      >
        {children}
        <TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
      </TooltipPrimitive.Content>
    </TooltipPrimitive.Portal>
  );
}

export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
</file>

<file path="src/components/analytics-tracker.tsx">
'use client';

import { useEffect } from 'react';
import { usePathname } from 'next/navigation';
import { pageview } from '@/lib/gtag'; 

export default function AnalyticsTracker() {
  const pathname = usePathname();

  useEffect(() => {
    // Registra la nueva ruta solo si la librería de GA está cargada
    if (typeof window.gtag !== 'undefined') {
      pageview(pathname);
    }
  }, [pathname]);

  return null;
}
</file>

<file path="src/components/CanvasDateElement.tsx">
// src/components/CanvasDateElement.tsx
import React, { useMemo } from 'react';
import { Text } from 'react-konva';
import { KonvaEventObject } from 'konva/lib/Node';
import Konva from 'konva'; // Necesario para los filtros

interface DateElement {
  id: string; text: string; format: string; x: number; y: number; fontSize: number; fill: string; rotation: number; fontFamily: string; opacity: number; blurRadius: number; shadowEnabled: boolean; shadowColor: string; shadowBlur: number; shadowOffsetX: number; shadowOffsetY: number; shadowOpacity: number; reflectionEnabled: boolean; flipX: number; flipY: number; filter: 'none' | 'grayscale' | 'sepia';
}

interface CanvasDateElementProps {
  dateElement: DateElement;
  isSelected: boolean;
  onSelect: () => void;
  onTransformEnd: (e: KonvaEventObject<Event>) => void;
  onDragEnd: (e: KonvaEventObject<Event>) => void;
  elementRef: (node: any) => void;
}

const CanvasDateElement: React.FC<CanvasDateElementProps> = ({
  dateElement,
  onSelect,
  onTransformEnd,
  onDragEnd,
  elementRef,
}) => {

  const getFormattedDate = (format: string, date: Date) => {
    return format
      .replace('dd', String(date.getDate()).padStart(2, '0'))
      .replace('MM', String(date.getMonth() + 1).padStart(2, '0'))
      .replace('yyyy', String(date.getFullYear()));
  };

  const currentDateText = useMemo(() => getFormattedDate(dateElement.format, new Date()), [dateElement.format]);

  // Helper para aplicar filtros
  const getFilter = (filter: 'none' | 'grayscale' | 'sepia', blurRadius: number) => {
    const filters: any[] = [];
    if (blurRadius > 0) {
      filters.push(Konva.Filters.Blur);
    }
    if (filter === 'grayscale') {
      filters.push(Konva.Filters.Grayscale);
    } else if (filter === 'sepia') {
      filters.push(Konva.Filters.Sepia);
    }
    return filters;
  };

  const commonProps = {
    text: currentDateText,
    x: dateElement.x,
    y: dateElement.y,
    fontSize: dateElement.fontSize,
    fontFamily: dateElement.fontFamily,
    fill: dateElement.fill,
    align: 'center' as const, // Forzado a centro para las fechas
    verticalAlign: "middle" as const,
    rotation: dateElement.rotation,
    opacity: dateElement.opacity,
    scaleX: dateElement.flipX,
    scaleY: dateElement.flipY,
    shadowEnabled: dateElement.shadowEnabled,
    shadowColor: dateElement.shadowColor,
    shadowBlur: dateElement.shadowBlur,
    shadowOffsetX: dateElement.shadowOffsetX,
    shadowOffsetY: dateElement.shadowOffsetY,
    shadowOpacity: dateElement.shadowOpacity,
    draggable: true,
    onClick: onSelect,
    onTap: onSelect,
    onDragEnd: onDragEnd,
    onTransformEnd: onTransformEnd,
    filters: getFilter(dateElement.filter, dateElement.blurRadius),
    blurRadius: dateElement.blurRadius,
    name: 'date-el',
    id: dateElement.id,
    ref: elementRef,
  };

  return (
    <>
      {dateElement.reflectionEnabled && (
        <Text
          {...commonProps}
          // Ajustes para la reflexión
          y={dateElement.y + (dateElement.fontSize * dateElement.flipY)} // Posición reflejada
          scaleY={dateElement.flipY * -1} // Invertir verticalmente
          opacity={dateElement.opacity * 0.3} // Opacidad baja para el reflejo
          rotation={dateElement.rotation * -1} // Rotación reflejada (a veces se ve mejor)
          listening={false} // No interactuar
        />
      )}
      <Text
        {...commonProps}
      />
    </>
  );
};

export default CanvasDateElement;
</file>

<file path="src/components/CanvasEditor.tsx">
// src/components/CanvasEditor.tsx
'use client';
import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react';
import { Stage, Layer, Image as KonvaImage, Text, Rect, Circle, Line, Transformer, Group } from 'react-konva';
import useImage from 'use-image';
import Konva from 'konva';

type FilterType = 'none' | 'grayscale' | 'sepia';
type ElementType = 'product' | 'background' | 'text' | 'shape' | 'date' | 'image' | 'sticker' | null;

interface TextElement {
  id: string;
  text: string;
  x: number;
  y: number;
  fontSize: number;
  fill: string;
  rotation: number;
  fontFamily: string;
  align: 'left' | 'center' | 'right';
  opacity: number;
  blurRadius: number;
  shadowEnabled: boolean;
  shadowColor: string;
  shadowBlur: number;
  shadowOffsetX: number;
  shadowOffsetY: number;
  shadowOpacity: number;
  reflectionEnabled: boolean;
  flipX: number;
  flipY: number;
  filter: FilterType;
  textDecoration?: 'none' | 'underline' | 'line-through' | 'bold';
  fontStyle?: 'normal' | 'italic';
  stroke?: string;
  strokeWidth?: number;
}

interface ShapeElement {
  id: string;
  type: 'rect' | 'circle' | 'line';
  x: number;
  y: number;
  width?: number;
  height?: number;
  radius?: number;
  fill?: string;
  stroke: string;
  strokeWidth: number;
  rotation: number;
  points?: number[];
  opacity: number;
  blurRadius: number;
  shadowEnabled: boolean;
  shadowColor: string;
  shadowBlur: number;
  shadowOffsetX: number;
  shadowOffsetY: number;
  shadowOpacity: number;
  reflectionEnabled: boolean;
  flipX: number;
  flipY: number;
  filter: FilterType;
}

interface DateElement {
  id: string;
  text: string;
  format: string;
  x: number;
  y: number;
  fontSize: number;
  fill: string;
  rotation: number;
  fontFamily: string;
  opacity: number;
  blurRadius: number;
  shadowEnabled: boolean;
  shadowColor: string;
  shadowBlur: number;
  shadowOffsetX: number;
  shadowOffsetY: number;
  shadowOpacity: number;
  reflectionEnabled: boolean;
  flipX: number;
  flipY: number;
  filter: FilterType;
}

interface ImageElement {
  id: string;
  url: string;
  x: number;
  y: number;
  scaleX: number;
  scaleY: number;
  rotation: number;
  opacity: number;
  blurRadius: number;
  shadowEnabled: boolean;
  shadowColor: string;
  shadowBlur: number;
  shadowOffsetX: number;
  shadowOffsetY: number;
  shadowOpacity: number;
  reflectionEnabled: boolean;
  flipX: number;
  flipY: number;
  filter: FilterType;
  width?: number;
  height?: number;
}

interface StickerElement {
  id: string;
  type: 'PROMO' | 'OFERTA' | 'DESCUENTO' | 'NEW' | 'SALE';
  x: number;
  y: number;
  rotation: number;
  opacity: number;
  scaleX: number;
  scaleY: number;
}

interface CanvasEditorProps {
  canvasWidth: number;
  canvasHeight: number;
  productImageUrl: string;
  backgroundUrl?: string;
  canvasBackgroundColor?: string;
  productX: number;
  productY: number;
  productScale: number;
  productRotation: number;
  productOpacity: number;
  productBlurRadius: number;
  productShadowEnabled: boolean;
  productShadowColor: string;
  productShadowBlur: number;
  productShadowOffsetX: number;
  productShadowOffsetY: number;
  productShadowOpacity: number;
  productReflectionEnabled: boolean;
  productFlipX: number;
  productFlipY: number;
  productFilter: FilterType;
  setProductX: (v: number) => void;
  setProductY: (v: number) => void;
  setProductScale: (v: number) => void;
  setHasProductBeenScaledManually: (v: boolean) => void;
  onProductRotate: (r: number) => void;
  backgroundOpacity: number;
  backgroundBlurRadius: number;
  backgroundShadowEnabled: boolean;
  backgroundShadowColor: string;
  backgroundShadowBlur: number;
  backgroundShadowOffsetX: number;
  backgroundShadowOffsetY: number;
  backgroundShadowOpacity: number;
  backgroundReflectionEnabled: boolean;
  backgroundFlipX: number;
  backgroundFlipY: number;
  backgroundFilter: FilterType;
  selectedCanvasElement: ElementType;
  selectedElementId: string | null;
  onElementSelect: (type: ElementType, id?: string) => void;
  textElements: TextElement[];
  onTextUpdate: (id: string, u: Partial<TextElement>) => void;
  shapeElements: ShapeElement[];
  onShapeUpdate: (id: string, u: Partial<ShapeElement>) => void;
  dateElement: DateElement | null;
  onDateUpdate: (u: Partial<DateElement>) => void;
  imageElements: ImageElement[];
  onImageUpdate: (id: string, u: Partial<ImageElement>) => void;
  onImageAddedAndLoaded: (id: string, scaleX: number, scaleY: number, x: number, y: number, w: number, h: number) => void;
  stickerElements: StickerElement[];
  onStickerUpdate: (id: string, u: Partial<StickerElement>) => void;
  onTransformEndCommit: () => void;
  onCanvasReady: (canvas: HTMLCanvasElement | null) => void;
  showCenterGuides: boolean;
  zOrderAction: { type: 'up' | 'down' | 'top' | 'bottom'; id: string; elementType: Exclude<ElementType, null>; } | null;
  onZOrderActionComplete: () => void;
  imageToMoveToBottomId: string | null;
  onImageMovedToBottom: (id: string) => void;
  onEnhanceComplete?: (newImageUrl: string) => void;
}

function getFilters(filter: FilterType, blurRadius: number): Function[] {
  const f: Function[] = [];
  if (blurRadius > 0) f.push(Konva.Filters.Blur);
  if (filter === 'grayscale') f.push(Konva.Filters.Grayscale);
  if (filter === 'sepia') f.push(Konva.Filters.Sepia);
  return f;
}

const MAX_IMG_SIZE = 300;

const ENHANCE_MESSAGES = [
  'Reconstruyendo píxeles...',
  'Mejorando rostro...',
  'Finalizando alta resolución...',
  'Analizando texturas...',
  'Aplicando súper resolución...',
];

const StickerBadge: React.FC<{ type: string; size: number }> = ({ type, size }) => {
  const colors: Record<string, { bg: string; text: string }> = {
    PROMO: { bg: '#ef4444', text: '#ffffff' },
    OFERTA: { bg: '#f59e0b', text: '#ffffff' },
    DESCUENTO: { bg: '#10b981', text: '#ffffff' },
    NEW: { bg: '#3b82f6', text: '#ffffff' },
    SALE: { bg: '#8b5cf6', text: '#ffffff' },
  };
  
  const color = colors[type] || colors.PROMO;
  
  return (
    <Group>
      <Circle radius={size / 2} fill={color.bg} />
      <Text
        text={type}
        fontSize={size * 0.25}
        fill={color.text}
        align="center"
        verticalAlign="middle"
        width={size}
        height={size}
        offsetX={size / 2}
        offsetY={size / 2}
        fontStyle="bold"
      />
    </Group>
  );
};

interface KonvaImageElProps {
  el: ImageElement;
  onSelect: () => void;
  onDragEnd: (e: any) => void;
  onTransformEnd: (e: any) => void;
  nodeRef: (node: Konva.Image | null) => void;
  onLoaded: (id: string, w: number, h: number) => void;
}

const KonvaImageEl: React.FC<KonvaImageElProps> = ({ el, onSelect, onDragEnd, onTransformEnd, nodeRef, onLoaded }) => {
  const [img] = useImage(el.url, 'anonymous');
  const imageRef = useRef<Konva.Image | null>(null);

  useEffect(() => {
    if (img && (!el.width || !el.height)) {
      const maxDim = Math.max(img.width, img.height);
      const scale = maxDim > MAX_IMG_SIZE ? MAX_IMG_SIZE / maxDim : 1;
      onLoaded(el.id, img.width * scale, img.height * scale);
    }
  }, [img, el.id, el.width, el.height, onLoaded]);

  useEffect(() => {
    if (imageRef.current) {
      const hasFilters = el.filter !== 'none' || el.blurRadius > 0;
      if (hasFilters) {
        imageRef.current.cache();
      } else {
        imageRef.current.clearCache();
      }
    }
  }, [el.filter, el.blurRadius]);

  if (!img) return null;

  const w = el.width ?? img.width;
  const h = el.height ?? img.height;
  const filters = getFilters(el.filter, el.blurRadius);

  const commonProps = {
    image: img,
    x: el.x,
    y: el.y,
    width: w,
    height: h,
    scaleX: el.scaleX * el.flipX,
    scaleY: el.scaleY * el.flipY,
    rotation: el.rotation,
    offsetX: w / 2,
    offsetY: h / 2,
    opacity: el.opacity,
    filters,
    blurRadius: el.blurRadius,
    shadowEnabled: el.shadowEnabled,
    shadowColor: el.shadowColor,
    shadowBlur: el.shadowBlur,
    shadowOffsetX: el.shadowOffsetX,
    shadowOffsetY: el.shadowOffsetY,
    shadowOpacity: el.shadowOpacity,
    perfectDrawEnabled: false,
  };

  return (
    <React.Fragment>
      {el.reflectionEnabled && (
        <KonvaImage
          {...commonProps}
          y={el.y + h * el.scaleY * el.flipY}
          scaleY={commonProps.scaleY * -1}
          opacity={el.opacity * 0.3}
          listening={false}
        />
      )}
      <KonvaImage
        {...commonProps}
        draggable
        name="image-el"
        id={el.id}
        onClick={onSelect}
        onTap={onSelect}
        onDragEnd={onDragEnd}
        onTransformEnd={onTransformEnd}
        ref={(node) => {
          imageRef.current = node;
          nodeRef(node);
        }}
      />
    </React.Fragment>
  );
};

const CanvasEditor: React.FC<CanvasEditorProps> = (props) => {
  const {
    canvasWidth,
    canvasHeight,
    productImageUrl,
    backgroundUrl,
    canvasBackgroundColor = '#ffffff',
    productX,
    productY,
    productScale,
    productRotation,
    productOpacity,
    productBlurRadius,
    productShadowEnabled,
    productShadowColor,
    productShadowBlur,
    productShadowOffsetX,
    productShadowOffsetY,
    productShadowOpacity,
    productReflectionEnabled,
    productFlipX,
    productFlipY,
    productFilter,
    setProductX,
    setProductY,
    setProductScale,
    setHasProductBeenScaledManually,
    onProductRotate,
    backgroundOpacity,
    backgroundBlurRadius,
    backgroundShadowEnabled,
    backgroundShadowColor,
    backgroundShadowBlur,
    backgroundShadowOffsetX,
    backgroundShadowOffsetY,
    backgroundShadowOpacity,
    backgroundReflectionEnabled,
    backgroundFlipX,
    backgroundFlipY,
    backgroundFilter,
    selectedCanvasElement,
    selectedElementId,
    onElementSelect,
    onTransformEndCommit,
    onCanvasReady,
    textElements,
    onTextUpdate,
    shapeElements,
    onShapeUpdate,
    dateElement,
    onDateUpdate,
    imageElements,
    onImageUpdate,
    onImageAddedAndLoaded,
    stickerElements,
    onStickerUpdate,
    showCenterGuides,
    zOrderAction,
    onZOrderActionComplete,
    imageToMoveToBottomId,
    onImageMovedToBottom,
    onEnhanceComplete,
  } = props;

  const stageRef = useRef<Konva.Stage | null>(null);
  const transformerRef = useRef<Konva.Transformer | null>(null);
  const productRef = useRef<Konva.Image | null>(null);
  const textRefs = useRef<Record<string, Konva.Text | null>>({});
  const shapeRefs = useRef<Record<string, Konva.Shape | null>>({});
  const imageRefs = useRef<Record<string, Konva.Image | null>>({});
  const stickerRefs = useRef<Record<string, Konva.Group | null>>({});
  const dateRef = useRef<Konva.Text | null>(null);
  const [productImage] = useImage(productImageUrl, 'anonymous');
  const [bgHTMLImage, setBgHTMLImage] = useState<HTMLImageElement | null>(null);

  // ESTADOS DE MEJORA IA (CLAUDE)
  const [isEnhancing, setIsEnhancing] = useState(false);
  const [enhanceMessage, setEnhanceMessage] = useState('');
  const [enhancedImageUrl, setEnhancedImageUrl] = useState<string | null>(null);
  const [originalImageUrl, setOriginalImageUrl] = useState<string | null>(null);

  // FUNCIÓN DE MEJORA IA (CLAUDE)
  const handleEnhanceImage = useCallback(async () => {
    const imageUrl = productImageUrl;
    if (!imageUrl) return;

    setIsEnhancing(true);
    setEnhanceMessage(ENHANCE_MESSAGES[0]);

    const intervalId = setInterval(() => {
      setEnhanceMessage(ENHANCE_MESSAGES[Math.floor(Math.random() * ENHANCE_MESSAGES.length)]);
    }, 3500);

    try {
      const res = await fetch(imageUrl);
      const blob = await res.blob();
      const file = new File([blob], 'product.png', { type: blob.type || 'image/png' });

      const formData = new FormData();
      formData.append('image', file);

      const response = await fetch('/api/ai/enhance-image', {
        method: 'POST',
        body: formData,
      });

      if (!response.ok) {
        const errData = await response.json() as { message?: string };
        throw new Error(errData.message || 'Error enhancing image');
      }

      const data = await response.json() as {
        preview: string;
        enhancedUrl: string;
        previewWidth: number;
        previewHeight: number;
      };

      setOriginalImageUrl(imageUrl);
      setEnhancedImageUrl(data.enhancedUrl);
      onEnhanceComplete?.(data.enhancedUrl);

    } catch (err: any) {
      console.error('[CanvasEditor] enhance error:', err);
    } finally {
      clearInterval(intervalId);
      setIsEnhancing(false);
    }
  }, [productImageUrl, onEnhanceComplete]);

  // CARGA DE FONDO
  useEffect(() => {
    if (!backgroundUrl) {
      setBgHTMLImage(null);
      return;
    }
    let cancelled = false;
    const img = new window.Image();
    img.crossOrigin = 'anonymous';
    img.onload = () => {
      if (!cancelled) setBgHTMLImage(img);
    };
    img.onerror = () => {
      if (cancelled) return;
      const img2 = new window.Image();
      img2.onload = () => {
        if (!cancelled) setBgHTMLImage(img2);
      };
      img2.onerror = () => {
        if (!cancelled) setBgHTMLImage(null);
      };
      img2.src = backgroundUrl;
    };
    img.src = backgroundUrl;
    return () => {
      cancelled = true;
    };
  }, [backgroundUrl]);

  // CANVAS READY
  useEffect(() => {
    if (!stageRef.current) return;
    const container = stageRef.current.container();
    const canvas = container?.querySelector('canvas') as HTMLCanvasElement | null;
    onCanvasReady(canvas);
  });

  // NODO SELECCIONADO
  const selectedNode = useMemo(() => {
    if (selectedCanvasElement === 'product') return productRef.current;
    if (!selectedElementId) return null;
    if (selectedCanvasElement === 'text') return textRefs.current[selectedElementId] ?? null;
    if (selectedCanvasElement === 'shape') return shapeRefs.current[selectedElementId] ?? null;
    if (selectedCanvasElement === 'image') return imageRefs.current[selectedElementId] ?? null;
    if (selectedCanvasElement === 'sticker') return stickerRefs.current[selectedElementId] ?? null;
    if (selectedCanvasElement === 'date') return dateRef.current;
    return null;
  }, [selectedCanvasElement, selectedElementId]);

  // TRANSFORMER
  useEffect(() => {
    const tr = transformerRef.current;
    if (!tr) return;
    if (selectedNode) {
      const isTextLike = selectedCanvasElement === 'text' || selectedCanvasElement === 'date';
      tr.setAttrs({
        enabledAnchors: isTextLike
          ? []
          : ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'middle-left', 'middle-right', 'top-center', 'bottom-center'],
        rotateEnabled: true,
      });
      tr.nodes([selectedNode as any]);
    } else {
      tr.nodes([]);
    }
    tr.getLayer()?.batchDraw();
  }, [selectedNode, selectedCanvasElement]);

  // Z-ORDER
  useEffect(() => {
    if (!zOrderAction) return;
    const { id, elementType, type } = zOrderAction;
    let node: Konva.Node | null = null;
    if (elementType === 'product') node = productRef.current;
    else if (elementType === 'text') node = textRefs.current[id] ?? null;
    else if (elementType === 'shape') node = shapeRefs.current[id] ?? null;
    else if (elementType === 'image') node = imageRefs.current[id] ?? null;
    else if (elementType === 'sticker') node = stickerRefs.current[id] ?? null;
    else if (elementType === 'date') node = dateRef.current;
    if (node) {
      if (type === 'up') node.moveUp();
      else if (type === 'down') node.moveDown();
      else if (type === 'top') node.moveToTop();
      else if (type === 'bottom') node.moveToBottom();
      node.getLayer()?.batchDraw();
    }
    onZOrderActionComplete();
  }, [zOrderAction, onZOrderActionComplete]);

  // MOVER AL FONDO
  useEffect(() => {
    if (!imageToMoveToBottomId) return;
    const node = imageRefs.current[imageToMoveToBottomId];
    if (node) {
      node.moveToBottom();
      node.getLayer()?.batchDraw();
      onImageMovedToBottom(imageToMoveToBottomId);
    }
  }, [imageToMoveToBottomId, onImageMovedToBottom]);

  // GUÍAS DE CENTRO
  useEffect(() => {
    if (!stageRef.current) return;
    const layer = stageRef.current.findOne('#guidesLayer') as Konva.Layer | null;
    if (!layer) return;
    layer.destroyChildren();
    if (showCenterGuides) {
      const s = { stroke: '#10b981', strokeWidth: 1, dash: [4, 4], listening: false };
      layer.add(new Konva.Line({ points: [canvasWidth / 2, 0, canvasWidth / 2, canvasHeight], ...s }));
      layer.add(new Konva.Line({ points: [0, canvasHeight / 2, canvasWidth, canvasHeight / 2], ...s }));
    }
    layer.batchDraw();
  }, [canvasWidth, canvasHeight, showCenterGuides]);

  // HANDLERS PRODUCTO
  const handleProductDragEnd = useCallback((e: any) => {
    const node = e.target as Konva.Image;
    setProductX(node.x());
    setProductY(node.y());
    setHasProductBeenScaledManually(true);
    onTransformEndCommit();
  }, [setProductX, setProductY, setHasProductBeenScaledManually, onTransformEndCommit]);

  const handleProductTransformEnd = useCallback((_e: any) => {
    const node = productRef.current;
    if (!node) return;
    const rawScaleX = node.scaleX() / productFlipX;
    const rawScaleY = node.scaleY() / productFlipY;
    const newScale = (Math.abs(rawScaleX) + Math.abs(rawScaleY)) / 2;
    setProductX(node.x());
    setProductY(node.y());
    setProductScale(newScale);
    onProductRotate(node.rotation());
    setHasProductBeenScaledManually(true);
    node.scaleX(newScale * productFlipX);
    node.scaleY(newScale * productFlipY);
    onTransformEndCommit();
  }, [productFlipX, productFlipY, setProductX, setProductY, setProductScale, onProductRotate, setHasProductBeenScaledManually, onTransformEndCommit]);

  // HANDLERS ELEMENTOS GENERALES
  const handleElementDragEnd = useCallback((e: any, id: string, type: 'text' | 'shape' | 'date' | 'image' | 'sticker') => {
    const node = e.target;
    const x = node.x(), y = node.y();
    if (type === 'text') onTextUpdate(id, { x, y });
    else if (type === 'shape') onShapeUpdate(id, { x, y });
    else if (type === 'date') onDateUpdate({ x, y });
    else if (type === 'image') onImageUpdate(id, { x, y });
    else if (type === 'sticker') onStickerUpdate(id, { x, y });
    onTransformEndCommit();
  }, [onTextUpdate, onShapeUpdate, onDateUpdate, onImageUpdate, onStickerUpdate, onTransformEndCommit]);

  const handleElementTransformEnd = useCallback((e: any, id: string, type: 'text' | 'shape' | 'date' | 'image' | 'sticker') => {
    const node = e.target;
    const x = node.x(), y = node.y(), rotation = node.rotation();
    const scaleX = node.scaleX(), scaleY = node.scaleY();
    if (type === 'text') {
      onTextUpdate(id, { x, y, rotation });
    } else if (type === 'shape') {
      const el = shapeElements.find(s => s.id === id);
      if (!el) return;
      if (el.type === 'rect') {
        onShapeUpdate(id, { x, y, rotation, width: (el.width ?? 100) * Math.abs(scaleX), height: (el.height ?? 100) * Math.abs(scaleY) });
        node.scaleX(el.flipX);
        node.scaleY(el.flipY);
      } else if (el.type === 'circle') {
        onShapeUpdate(id, { x, y, rotation, radius: (el.radius ?? 50) * Math.abs(scaleX) });
        node.scaleX(el.flipX);
        node.scaleY(el.flipY);
      } else {
        onShapeUpdate(id, { x, y, rotation });
      }
    } else if (type === 'date') {
      onDateUpdate({ x, y, rotation });
    } else if (type === 'image') {
      onImageUpdate(id, { x, y, rotation, scaleX, scaleY });
    } else if (type === 'sticker') {
      onStickerUpdate(id, { x, y, rotation, scaleX, scaleY });
    }
    onTransformEndCommit();
  }, [shapeElements, onTextUpdate, onShapeUpdate, onDateUpdate, onImageUpdate, onStickerUpdate, onTransformEndCommit]);

  const handleStageClick = useCallback((e: any) => {
    const name = e.target.name();
    if (e.target === e.target.getStage() || name === 'backgroundRect') {
      onElementSelect(null);
      return;
    }
    if (name === 'product') onElementSelect('product');
    else if (name === 'text-el') onElementSelect('text', e.target.id());
    else if (name === 'shape-el') onElementSelect('shape', e.target.id());
    else if (name === 'date-el') onElementSelect('date', e.target.id());
    else if (name === 'image-el') onElementSelect('image', e.target.id());
    else if (name === 'sticker-el') onElementSelect('sticker', e.target.id());
    else if (name === 'bg-image') onElementSelect('background');
  }, [onElementSelect]);

  const handleImageLoaded = useCallback((id: string, w: number, h: number) => {
    onImageAddedAndLoaded(id, 1, 1, canvasWidth / 2, canvasHeight / 2, w, h);
  }, [onImageAddedAndLoaded, canvasWidth, canvasHeight]);

  const bgFilters = getFilters(backgroundFilter, backgroundBlurRadius);
  const prodFilters = getFilters(productFilter, productBlurRadius);
  const showChecker = canvasBackgroundColor === 'transparent' || canvasBackgroundColor === '';

  return (
    <div
      id="canvas-container"
      style={{
        width: canvasWidth,
        height: canvasHeight,
        position: 'relative',
        boxShadow: '0 8px 32px rgba(0,0,0,0.15)',
        borderRadius: 8,
        overflow: 'hidden',
        backgroundImage: showChecker
          ? 'linear-gradient(45deg,#ccc 25%,transparent 25%),linear-gradient(-45deg,#ccc 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#ccc 75%),linear-gradient(-45deg,transparent 75%,#ccc 75%)'
          : undefined,
        backgroundSize: showChecker ? '16px 16px' : undefined,
        backgroundPosition: showChecker ? '0 0,0 8px,8px -8px,-8px 0' : undefined,
        backgroundColor: showChecker ? '#f0f0f0' : undefined,
      }}
    >
      <Stage width={canvasWidth} height={canvasHeight} ref={stageRef} onClick={handleStageClick} onTap={handleStageClick}>
        <Layer id="backgroundLayer">
          <Rect
            x={0}
            y={0}
            width={canvasWidth}
            height={canvasHeight}
            fill={showChecker ? 'transparent' : canvasBackgroundColor}
            name="backgroundRect"
            onClick={() => onElementSelect('background')}
            onTap={() => onElementSelect('background')}
            perfectDrawEnabled={false}
          />

          {bgHTMLImage && (
            <React.Fragment>
              {backgroundReflectionEnabled && (
                <KonvaImage
                  image={bgHTMLImage}
                  x={canvasWidth / 2}
                  y={canvasHeight}
                  width={canvasWidth}
                  height={canvasHeight}
                  offsetX={canvasWidth / 2}
                  offsetY={canvasHeight / 2}
                  scaleX={backgroundFlipX}
                  scaleY={backgroundFlipY * -1}
                  opacity={backgroundOpacity * 0.3}
                  filters={bgFilters}
                  blurRadius={backgroundBlurRadius}
                  listening={false}
                  perfectDrawEnabled={false}
                />
              )}
              <KonvaImage
                image={bgHTMLImage}
                x={0}
                y={0}
                width={canvasWidth}
                height={canvasHeight}
                scaleX={backgroundFlipX}
                scaleY={backgroundFlipY}
                opacity={backgroundOpacity}
                filters={bgFilters}
                blurRadius={backgroundBlurRadius}
                shadowEnabled={backgroundShadowEnabled}
                shadowColor={backgroundShadowColor}
                shadowBlur={backgroundShadowBlur}
                shadowOffsetX={backgroundShadowOffsetX}
                shadowOffsetY={backgroundShadowOffsetY}
                shadowOpacity={backgroundShadowOpacity}
                name="bg-image"
                onClick={() => onElementSelect('background')}
                onTap={() => onElementSelect('background')}
                perfectDrawEnabled={false}
              />
            </React.Fragment>
          )}

          {productImage && (
            <React.Fragment>
              {productReflectionEnabled && (
                <KonvaImage
                  image={productImage}
                  x={productX}
                  y={productY + productImage.height * productScale * productFlipY}
                  width={productImage.width}
                  height={productImage.height}
                  scaleX={productScale * productFlipX}
                  scaleY={productScale * productFlipY * -1}
                  rotation={productRotation}
                  opacity={productOpacity * 0.3}
                  offsetX={productImage.width / 2}
                  offsetY={productImage.height / 2}
                  filters={prodFilters}
                  blurRadius={productBlurRadius}
                  listening={false}
                  perfectDrawEnabled={false}
                />
              )}
              <KonvaImage
                image={productImage}
                x={productX}
                y={productY}
                width={productImage.width}
                height={productImage.height}
                scaleX={productScale * productFlipX}
                scaleY={productScale * productFlipY}
                rotation={productRotation}
                opacity={productOpacity}
                offsetX={productImage.width / 2}
                offsetY={productImage.height / 2}
                draggable
                name="product"
                id="product-el"
                onClick={() => onElementSelect('product')}
                onTap={() => onElementSelect('product')}
                onDragEnd={handleProductDragEnd}
                onTransformEnd={handleProductTransformEnd}
                ref={productRef}
                filters={prodFilters}
                blurRadius={productBlurRadius}
                shadowEnabled={productShadowEnabled}
                shadowColor={productShadowColor}
                shadowBlur={productShadowBlur}
                shadowOffsetX={productShadowOffsetX}
                shadowOffsetY={productShadowOffsetY}
                shadowOpacity={productShadowOpacity}
                perfectDrawEnabled={false}
              />
            </React.Fragment>
          )}
        </Layer>

        <Layer id="elementsLayer">
          {imageElements.map(el => (
            <KonvaImageEl
              key={el.id}
              el={el}
              onSelect={() => onElementSelect('image', el.id)}
              onDragEnd={(e) => handleElementDragEnd(e, el.id, 'image')}
              onTransformEnd={(e) => handleElementTransformEnd(e, el.id, 'image')}
              nodeRef={(node) => {
                imageRefs.current[el.id] = node;
              }}
              onLoaded={handleImageLoaded}
            />
          ))}

          {stickerElements.map(el => (
            <Group
              key={el.id}
              x={el.x}
              y={el.y}
              rotation={el.rotation}
              scaleX={el.scaleX}
              scaleY={el.scaleY}
              opacity={el.opacity}
              draggable
              name="sticker-el"
              id={el.id}
              onClick={() => onElementSelect('sticker', el.id)}
              onTap={() => onElementSelect('sticker', el.id)}
              onDragEnd={(e) => handleElementDragEnd(e, el.id, 'sticker')}
              onTransformEnd={(e) => handleElementTransformEnd(e, el.id, 'sticker')}
              ref={(node) => {
                stickerRefs.current[el.id] = node;
              }}
            >
              <StickerBadge type={el.type} size={100} />
            </Group>
          ))}

          {shapeElements.map(el => {
            const filters = getFilters(el.filter, el.blurRadius);
            const shadow = {
              shadowEnabled: el.shadowEnabled,
              shadowColor: el.shadowColor,
              shadowBlur: el.shadowBlur,
              shadowOffsetX: el.shadowOffsetX,
              shadowOffsetY: el.shadowOffsetY,
              shadowOpacity: el.shadowOpacity,
            };
            const cp = {
              x: el.x,
              y: el.y,
              rotation: el.rotation,
              opacity: el.opacity,
              scaleX: el.flipX,
              scaleY: el.flipY,
              fill: el.fill,
              stroke: el.stroke,
              strokeWidth: el.strokeWidth,
              filters,
              blurRadius: el.blurRadius,
              ...shadow,
              draggable: true,
              name: 'shape-el',
              id: el.id,
              onClick: () => onElementSelect('shape', el.id),
              onTap: () => onElementSelect('shape', el.id),
              onDragEnd: (e: any) => handleElementDragEnd(e, el.id, 'shape'),
              onTransformEnd: (e: any) => handleElementTransformEnd(e, el.id, 'shape'),
              ref: (node: any) => {
                shapeRefs.current[el.id] = node;
              },
              perfectDrawEnabled: false,
            };

            if (el.type === 'rect') {
              const w = el.width ?? 100, h = el.height ?? 100;
              return (
                <React.Fragment key={el.id}>
                  {el.reflectionEnabled && (
                    <Rect
                      {...cp}
                      y={el.y + h * el.flipY}
                      width={w}
                      height={h}
                      offsetX={w / 2}
                      offsetY={h / 2}
                      scaleY={el.flipY * -1}
                      opacity={el.opacity * 0.3}
                      listening={false}
                      draggable={false}
                    />
                  )}
                  <Rect {...cp} width={w} height={h} offsetX={w / 2} offsetY={h / 2} />
                </React.Fragment>
              );
            }
            if (el.type === 'circle') {
              const r = el.radius ?? 50;
              return (
                <React.Fragment key={el.id}>
                  {el.reflectionEnabled && (
                    <Circle
                      {...cp}
                      y={el.y + r * 2 * el.flipY}
                      radius={r}
                      scaleY={el.flipY * -1}
                      opacity={el.opacity * 0.3}
                      listening={false}
                      draggable={false}
                    />
                  )}
                  <Circle {...cp} radius={r} />
                </React.Fragment>
              );
            }
            if (el.type === 'line') {
              return (
                <Line
                  key={el.id}
                  {...cp}
                  fill={undefined}
                  points={el.points ?? [0, 0, 100, 100]}
                  tension={0}
                  closed={false}
                  offsetX={0}
                  offsetY={0}
                />
              );
            }
            return null;
          })}

          {textElements.map(el => {
            const filters = getFilters(el.filter, el.blurRadius);
            const fontStyle = [
              el.textDecoration === 'bold' ? 'bold' : '',
              el.fontStyle === 'italic' ? 'italic' : '',
            ].filter(Boolean).join(' ') || 'normal';
            return (
              <React.Fragment key={el.id}>
                {el.reflectionEnabled && (
                  <Text
                    text={el.text}
                    x={el.x}
                    y={el.y + el.fontSize * el.flipY}
                    fontSize={el.fontSize}
                    fontFamily={el.fontFamily}
                    fill={el.fill}
                    align={el.align}
                    fontStyle={fontStyle}
                    rotation={el.rotation * -1}
                    scaleX={el.flipX}
                    scaleY={el.flipY * -1}
                    opacity={el.opacity * 0.3}
                    filters={filters}
                    blurRadius={el.blurRadius}
                    listening={false}
                    perfectDrawEnabled={false}
                  />
                )}
                <Text
                  text={el.text}
                  x={el.x}
                  y={el.y}
                  fontSize={el.fontSize}
                  fontFamily={el.fontFamily}
                  fill={el.fill}
                  align={el.align}
                  fontStyle={fontStyle}
                  textDecoration={el.textDecoration === 'underline' ? 'underline' : el.textDecoration === 'line-through' ? 'line-through' : 'none'}
                  stroke={el.stroke}
                  strokeWidth={el.strokeWidth ?? 0}
                  rotation={el.rotation}
                  opacity={el.opacity}
                  scaleX={el.flipX}
                  scaleY={el.flipY}
                  shadowEnabled={el.shadowEnabled}
                  shadowColor={el.shadowColor}
                  shadowBlur={el.shadowBlur}
                  shadowOffsetX={el.shadowOffsetX}
                  shadowOffsetY={el.shadowOffsetY}
                  shadowOpacity={el.shadowOpacity}
                  filters={filters}
                  blurRadius={el.blurRadius}
                  draggable
                  name="text-el"
                  id={el.id}
                  onClick={() => onElementSelect('text', el.id)}
                  onTap={() => onElementSelect('text', el.id)}
                  onDragEnd={(e) => handleElementDragEnd(e, el.id, 'text')}
                  onTransformEnd={(e) => handleElementTransformEnd(e, el.id, 'text')}
                  ref={(node) => {
                    textRefs.current[el.id] = node;
                  }}
                  perfectDrawEnabled={false}
                />
              </React.Fragment>
            );
          })}

          {dateElement && (
            <React.Fragment key={dateElement.id}>
              {dateElement.reflectionEnabled && (
                <Text
                  text={dateElement.text}
                  x={dateElement.x}
                  y={dateElement.y + dateElement.fontSize * dateElement.flipY}
                  fontSize={dateElement.fontSize}
                  fontFamily={dateElement.fontFamily}
                  fill={dateElement.fill}
                  align="center"
                  rotation={dateElement.rotation * -1}
                  scaleX={dateElement.flipX}
                  scaleY={dateElement.flipY * -1}
                  opacity={dateElement.opacity * 0.3}
                  listening={false}
                  perfectDrawEnabled={false}
                />
              )}
              <Text
                text={dateElement.text}
                x={dateElement.x}
                y={dateElement.y}
                fontSize={dateElement.fontSize}
                fontFamily={dateElement.fontFamily}
                fill={dateElement.fill}
                align="center"
                rotation={dateElement.rotation}
                opacity={dateElement.opacity}
                scaleX={dateElement.flipX}
                scaleY={dateElement.flipY}
                shadowEnabled={dateElement.shadowEnabled}
                shadowColor={dateElement.shadowColor}
                shadowBlur={dateElement.shadowBlur}
                shadowOffsetX={dateElement.shadowOffsetX}
                shadowOffsetY={dateElement.shadowOffsetY}
                shadowOpacity={dateElement.shadowOpacity}
                filters={getFilters(dateElement.filter, dateElement.blurRadius)}
                blurRadius={dateElement.blurRadius}
                draggable
                name="date-el"
                id={dateElement.id}
                onClick={() => onElementSelect('date', dateElement.id)}
                onTap={() => onElementSelect('date', dateElement.id)}
                onDragEnd={(e) => handleElementDragEnd(e, dateElement.id, 'date')}
                onTransformEnd={(e) => handleElementTransformEnd(e, dateElement.id, 'date')}
                ref={dateRef}
                perfectDrawEnabled={false}
              />
            </React.Fragment>
          )}

          <Transformer
            ref={transformerRef}
            rotateEnabled={true}
            keepRatio={selectedCanvasElement === 'product'}
            flipEnabled={false}
            boundBoxFunc={(oldBox, newBox) => {
              if (Math.abs(newBox.width) < 5 || Math.abs(newBox.height) < 5) return oldBox;
              return newBox;
            }}
          />
        </Layer>

        <Layer id="guidesLayer" listening={false} />
      </Stage>

      {/* BLOQUE DE BOTONES FLOTANTES (MEJORA IA) */}
      {productImageUrl && (
        <div className="absolute bottom-4 right-4 flex flex-col items-end gap-2 z-[50]">
          
          {/* Mensajes de carga animados */}
          {isEnhancing && (
            <div className="flex items-center gap-2 bg-black/70 backdrop-blur-md text-white text-xs font-medium px-3 py-2 rounded-full shadow-xl">
              <svg className="w-3.5 h-3.5 animate-spin" fill="none" viewBox="0 0 24 24">
                <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"/>
                <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"/>
              </svg>
              {enhanceMessage}
            </div>
          )}

          {/* Botón Principal Enhance */}
          <button
            onClick={handleEnhanceImage}
            disabled={isEnhancing}
            className={`flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-semibold shadow-lg transition-all duration-200 ${
              isEnhancing 
                ? 'bg-zinc-200 text-zinc-400 cursor-not-allowed' 
                : 'bg-gradient-to-br from-emerald-500 to-teal-500 text-white hover:shadow-emerald-300/50 hover:shadow-xl hover:-translate-y-0.5 active:translate-y-0'
            }`}
          >
            <svg className="w-4 h-4 shrink-0" fill="currentColor" viewBox="0 0 24 24">
              <path d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"/>
            </svg>
            {isEnhancing ? 'Mejorando…' : 'Mejorar imagen'}
          </button>

          {/* Botón Comparar (Before/After) */}
          {enhancedImageUrl && originalImageUrl && (
            <button
              onMouseDown={() => onEnhanceComplete?.(originalImageUrl)}
              onMouseUp={() => onEnhanceComplete?.(enhancedImageUrl)}
              onMouseLeave={() => onEnhanceComplete?.(enhancedImageUrl)}
              onTouchStart={() => onEnhanceComplete?.(originalImageUrl)}
              onTouchEnd={() => onEnhanceComplete?.(enhancedImageUrl)}
              className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-black/60 backdrop-blur-sm text-white text-xs font-medium hover:bg-black/70 transition-colors select-none shadow-lg"
            >
              <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
              </svg>
              Mantén para ver original
            </button>
          )}
        </div>
      )}
    </div>
  );
};

export default CanvasEditor;
</file>

</files>
