UNPKG

@toriistudio/v0-playground

Version:
2,406 lines 84.8 kB
// src/components/Playground.tsx
import { useEffect as useEffect7, useMemo as useMemo5, useState as useState5 } from "react";
import { Check as Check3, Copy as Copy2 } from "lucide-react";

// src/context/ResizableLayout.tsx
import {
  createContext,
  useContext,
  useRef,
  useState,
  useEffect
} from "react";
import { GripVertical } from "lucide-react";
import { jsx, jsxs } from "react/jsx-runtime";
var ResizableLayoutContext = createContext(
  null
);
var useResizableLayout = () => {
  const ctx = useContext(ResizableLayoutContext);
  if (!ctx) throw new Error("ResizableLayoutContext not found");
  return ctx;
};
var ResizableLayout = ({
  children,
  hideControls
}) => {
  const [leftPanelWidth, setLeftPanelWidth] = useState(25);
  const [isDesktop, setIsDesktop] = useState(false);
  const [isHydrated, setIsHydrated] = useState(false);
  const [isDragging, setIsDragging] = useState(false);
  const [sidebarNarrow, setSidebarNarrow] = useState(false);
  const containerRef = useRef(null);
  useEffect(() => {
    setIsHydrated(true);
    const handleResize = () => setIsDesktop(window.innerWidth >= 768);
    handleResize();
    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);
  useEffect(() => {
    if (!isHydrated || !isDesktop) return;
    const checkSidebarWidth = () => {
      if (containerRef.current) {
        const containerWidth = containerRef.current.clientWidth;
        const sidebarWidth = leftPanelWidth / 100 * containerWidth;
        setSidebarNarrow(sidebarWidth < 350);
      }
    };
    checkSidebarWidth();
    window.addEventListener("resize", checkSidebarWidth);
    return () => window.removeEventListener("resize", checkSidebarWidth);
  }, [leftPanelWidth, isHydrated, isDesktop]);
  useEffect(() => {
    const handleMouseMove = (e) => {
      if (isDragging && containerRef.current) {
        const containerRect = containerRef.current.getBoundingClientRect();
        const newLeftWidth = (e.clientX - containerRect.left) / containerRect.width * 100;
        if (newLeftWidth >= 20 && newLeftWidth <= 80) {
          setLeftPanelWidth(newLeftWidth);
        }
      }
    };
    const handleMouseUp = () => setIsDragging(false);
    if (isDragging) {
      document.addEventListener("mousemove", handleMouseMove);
      document.addEventListener("mouseup", handleMouseUp);
    }
    return () => {
      document.removeEventListener("mousemove", handleMouseMove);
      document.removeEventListener("mouseup", handleMouseUp);
    };
  }, [isDragging]);
  return /* @__PURE__ */ jsx(
    ResizableLayoutContext.Provider,
    {
      value: {
        leftPanelWidth,
        isHydrated,
        isDesktop,
        sidebarNarrow,
        containerRef
      },
      children: /* @__PURE__ */ jsx("div", { className: "min-h-screen w-full bg-black text-white", children: /* @__PURE__ */ jsxs(
        "div",
        {
          ref: containerRef,
          className: "flex flex-col md:flex-row min-h-screen w-full overflow-hidden select-none",
          children: [
            children,
            isHydrated && isDesktop && !hideControls && /* @__PURE__ */ jsx(
              "div",
              {
                className: "order-3 w-2 bg-stone-800 hover:bg-stone-700 cursor-col-resize items-center justify-center z-10 transition-opacity duration-300",
                onMouseDown: () => setIsDragging(true),
                style: {
                  position: "absolute",
                  left: `${leftPanelWidth}%`,
                  top: 0,
                  bottom: 0,
                  display: "flex"
                },
                children: /* @__PURE__ */ jsx(GripVertical, { className: "h-6 w-6 text-stone-500" })
              }
            )
          ]
        }
      ) })
    }
  );
};

// src/context/ControlsContext.tsx
import {
  createContext as createContext2,
  useContext as useContext2,
  useState as useState2,
  useMemo,
  useEffect as useEffect2,
  useCallback,
  useRef as useRef2
} from "react";

// src/utils/getUrlParams.ts
var getUrlParams = () => {
  if (typeof window === "undefined") return {};
  const params = new URLSearchParams(window.location.search);
  const entries = {};
  for (const [key, value] of params.entries()) {
    entries[key] = value;
  }
  return entries;
};

// src/constants/urlParams.ts
var NO_CONTROLS_PARAM = "nocontrols";
var PRESENTATION_PARAM = "presentation";
var CONTROLS_ONLY_PARAM = "controlsonly";

// src/utils/getControlsChannelName.ts
var EXCLUDED_KEYS = /* @__PURE__ */ new Set([
  NO_CONTROLS_PARAM,
  PRESENTATION_PARAM,
  CONTROLS_ONLY_PARAM
]);
var getControlsChannelName = () => {
  if (typeof window === "undefined") return null;
  const params = new URLSearchParams(window.location.search);
  for (const key of EXCLUDED_KEYS) {
    params.delete(key);
  }
  const query = params.toString();
  const base = window.location.pathname || "/";
  return `v0-controls:${base}${query ? `?${query}` : ""}`;
};

// src/lib/advancedPalette.ts
var CHANNEL_KEYS = ["r", "g", "b"];
var DEFAULT_CHANNEL_LABELS = {
  r: "Red",
  g: "Green",
  b: "Blue"
};
var DEFAULT_SECTIONS = [
  { key: "A", label: "Vector A", helper: "Base offset" },
  { key: "B", label: "Vector B", helper: "Amplitude" },
  { key: "C", label: "Vector C", helper: "Frequency" },
  { key: "D", label: "Vector D", helper: "Phase shift" }
];
var DEFAULT_RANGES = {
  A: { min: 0, max: 1, step: 0.01 },
  B: { min: -1, max: 1, step: 0.01 },
  C: { min: 0, max: 2, step: 0.01 },
  D: { min: 0, max: 1, step: 0.01 }
};
var DEFAULT_HIDDEN_KEY_PREFIX = "palette";
var DEFAULT_GRADIENT_STEPS = 12;
var DEFAULT_HEX_PALETTE = {
  A: { r: 0.5, g: 0.5, b: 0.5 },
  B: { r: 0.5, g: 0.5, b: 0.5 },
  C: { r: 1, g: 1, b: 1 },
  D: { r: 0, g: 0.1, b: 0.2 }
};
var createPaletteControlKey = (prefix, section, channel) => `${prefix}${section}${channel}`;
var clamp = (value, min, max) => Math.min(Math.max(value, min), max);
var clamp01 = (value) => clamp(value, 0, 1);
var toNumberOr = (value, fallback) => {
  if (typeof value === "number" && Number.isFinite(value)) return value;
  if (typeof value === "string") {
    const parsed = parseFloat(value);
    if (Number.isFinite(parsed)) return parsed;
  }
  return fallback;
};
var paletteColorAt = (palette, t) => {
  const twoPi = Math.PI * 2;
  const computeChannel = (a, b, c, d) => {
    const value = a + b * Math.cos(twoPi * (c * t + d));
    return clamp(value, 0, 1);
  };
  return {
    r: computeChannel(
      palette.A?.r ?? 0,
      palette.B?.r ?? 0,
      palette.C?.r ?? 0,
      palette.D?.r ?? 0
    ),
    g: computeChannel(
      palette.A?.g ?? 0,
      palette.B?.g ?? 0,
      palette.C?.g ?? 0,
      palette.D?.g ?? 0
    ),
    b: computeChannel(
      palette.A?.b ?? 0,
      palette.B?.b ?? 0,
      palette.C?.b ?? 0,
      palette.D?.b ?? 0
    )
  };
};
var toRgba = ({ r, g, b }, alpha = 0.5) => `rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(
  b * 255
)}, ${alpha})`;
var computePaletteGradient = (palette, steps = DEFAULT_GRADIENT_STEPS) => {
  const stops = Array.from({ length: steps }, (_, index) => {
    const t = index / (steps - 1);
    const color = paletteColorAt(palette, t);
    const stop = (t * 100).toFixed(1);
    return `${toRgba(color)} ${stop}%`;
  });
  return `linear-gradient(to right, ${stops.join(", ")})`;
};
var createPaletteSignature = (palette) => Object.entries(palette).sort(([aKey], [bKey]) => aKey.localeCompare(bKey)).flatMap(
  ([, channels]) => CHANNEL_KEYS.map((channel) => (channels?.[channel] ?? 0).toFixed(3))
).join("-");
var isAdvancedPaletteValue = (value) => Boolean(
  value && typeof value === "object" && CHANNEL_KEYS.every((channel) => {
    const channelValue = value[channel];
    return typeof channelValue === "number" && Number.isFinite(channelValue);
  })
);
var isAdvancedPalette = (value) => Boolean(
  value && typeof value === "object" && Object.values(value).every(
    (entry) => isAdvancedPaletteValue(entry) || typeof entry === "object"
  )
);
var normalizePaletteValue = (source) => {
  if (typeof source === "string") {
    return hexToPaletteValue(source);
  }
  const channelSource = source ?? {};
  const toChannel = (channel) => clamp01(
    toNumberOr(
      channelSource[channel],
      0
    )
  );
  return {
    r: toChannel("r"),
    g: toChannel("g"),
    b: toChannel("b")
  };
};
var createPaletteFromRecord = (record) => Object.entries(record).reduce((acc, [key, value]) => {
  acc[key] = normalizePaletteValue(value);
  return acc;
}, {});
var clonePalette = (palette) => Object.fromEntries(
  Object.entries(palette).map(([sectionKey, channels]) => [
    sectionKey,
    { ...channels }
  ])
);
var hexComponentToNormalized = (component) => clamp01(parseInt(component, 16) / 255 || 0);
var normalizedChannelToHex = (value) => Math.round(clamp01(value) * 255).toString(16).padStart(2, "0");
var sanitizeHex = (hex) => {
  let sanitized = hex.trim();
  if (sanitized.startsWith("#")) {
    sanitized = sanitized.slice(1);
  }
  if (sanitized.length === 3) {
    sanitized = sanitized.split("").map((char) => char + char).join("");
  }
  return sanitized.length === 6 ? sanitized : null;
};
var hexToPaletteValue = (hex) => {
  const sanitized = sanitizeHex(hex);
  if (!sanitized) {
    return { r: 0, g: 0, b: 0 };
  }
  return {
    r: hexComponentToNormalized(sanitized.slice(0, 2)),
    g: hexComponentToNormalized(sanitized.slice(2, 4)),
    b: hexComponentToNormalized(sanitized.slice(4, 6))
  };
};
var paletteValueToHex = (value) => `#${normalizedChannelToHex(value.r)}${normalizedChannelToHex(
  value.g
)}${normalizedChannelToHex(value.b)}`;
var createAdvancedPalette = (source = DEFAULT_HEX_PALETTE, options) => {
  if (Array.isArray(source)) {
    const order = options?.sectionOrder ?? DEFAULT_SECTIONS.map((section) => section.key);
    const record = {};
    source.forEach((value, index) => {
      const preferredKey = order[index];
      const fallbackKey = `Color${index + 1}`;
      const key = preferredKey && !(preferredKey in record) ? preferredKey : fallbackKey;
      record[key] = value;
    });
    return createPaletteFromRecord(record);
  }
  if (isAdvancedPalette(source)) {
    return clonePalette(
      Object.entries(source ?? {}).reduce(
        (acc, [key, value]) => {
          acc[key] = normalizePaletteValue(value);
          return acc;
        },
        {}
      )
    );
  }
  if (source && typeof source === "object") {
    return createPaletteFromRecord(source);
  }
  return createPaletteFromRecord(DEFAULT_HEX_PALETTE);
};
var DEFAULT_ADVANCED_PALETTE = createAdvancedPalette(
  DEFAULT_HEX_PALETTE
);
var advancedPaletteToHexColors = (palette, options) => {
  const fallbackPalette = options?.fallbackPalette ?? DEFAULT_ADVANCED_PALETTE;
  const orderedKeys = options?.sectionOrder ?? (Object.keys(palette).length > 0 ? Object.keys(palette) : Object.keys(fallbackPalette));
  const uniqueKeys = Array.from(new Set(orderedKeys));
  if (uniqueKeys.length === 0) {
    uniqueKeys.push(...Object.keys(DEFAULT_ADVANCED_PALETTE));
  }
  const defaultColor = options?.defaultColor ?? "#000000";
  return uniqueKeys.map((key) => {
    const paletteValue = palette[key] ?? fallbackPalette[key];
    if (!paletteValue) return defaultColor;
    return paletteValueToHex(paletteValue);
  });
};
var createDefaultSectionsFromPalette = (palette) => {
  const sectionKeys = Object.keys(palette);
  if (sectionKeys.length === 0) return DEFAULT_SECTIONS;
  return sectionKeys.map((key, index) => ({
    key,
    label: `Vector ${key}`,
    helper: DEFAULT_SECTIONS[index]?.helper ?? "Palette parameter"
  }));
};
var resolveAdvancedPaletteConfig = (config) => {
  const defaultPalette = createAdvancedPalette(config.defaultPalette);
  const sections = config.sections ?? createDefaultSectionsFromPalette(defaultPalette);
  const ranges = {};
  sections.forEach((section) => {
    ranges[section.key] = config.ranges?.[section.key] ?? DEFAULT_RANGES[section.key] ?? {
      min: 0,
      max: 1,
      step: 0.01
    };
  });
  const channelLabels = {
    ...DEFAULT_CHANNEL_LABELS,
    ...config.channelLabels ?? {}
  };
  return {
    ...config,
    defaultPalette,
    sections,
    ranges,
    channelLabels,
    hiddenKeyPrefix: config.hiddenKeyPrefix ?? DEFAULT_HIDDEN_KEY_PREFIX,
    controlKey: config.controlKey ?? "advancedPaletteControl",
    gradientSteps: config.gradientSteps ?? DEFAULT_GRADIENT_STEPS
  };
};
var createAdvancedPaletteSchemaEntries = (schema, resolvedConfig) => {
  const { sections, hiddenKeyPrefix, defaultPalette } = resolvedConfig;
  const updatedSchema = { ...schema };
  sections.forEach((section) => {
    CHANNEL_KEYS.forEach((channel) => {
      const key = createPaletteControlKey(
        hiddenKeyPrefix,
        section.key,
        channel
      );
      if (!(key in updatedSchema)) {
        updatedSchema[key] = {
          type: "number",
          value: defaultPalette?.[section.key]?.[channel] ?? DEFAULT_RANGES[section.key]?.min ?? 0,
          hidden: true
        };
      }
    });
  });
  return updatedSchema;
};

// src/context/ControlsContext.tsx
import { jsx as jsx2 } from "react/jsx-runtime";
var ControlsContext = createContext2(null);
var useControlsContext = () => {
  const ctx = useContext2(ControlsContext);
  if (!ctx) throw new Error("useControls must be used within ControlsProvider");
  return ctx;
};
var ControlsProvider = ({ children }) => {
  const [schema, setSchema] = useState2({});
  const [values, setValues] = useState2({});
  const [config, setConfig] = useState2({
    showCopyButton: true,
    showCodeSnippet: false
  });
  const [componentName, setComponentName] = useState2();
  const [channelName, setChannelName] = useState2(null);
  const channelRef = useRef2(null);
  const instanceIdRef = useRef2(null);
  const skipBroadcastRef = useRef2(false);
  const latestValuesRef = useRef2(values);
  useEffect2(() => {
    latestValuesRef.current = values;
  }, [values]);
  useEffect2(() => {
    if (typeof window === "undefined") return;
    setChannelName(getControlsChannelName());
  }, []);
  const setValue = (key, value) => {
    setValues((prev) => ({ ...prev, [key]: value }));
  };
  const registerSchema = (newSchema, opts) => {
    if (opts?.componentName) {
      setComponentName(opts.componentName);
    }
    if (opts?.config) {
      const {
        addAdvancedPaletteControl,
        addMediaUploadControl,
        ...otherConfig
      } = opts.config;
      setConfig((prev) => {
        const nextConfig = {
          ...prev,
          ...otherConfig
        };
        if (Object.prototype.hasOwnProperty.call(
          opts.config,
          "addAdvancedPaletteControl"
        )) {
          nextConfig.addAdvancedPaletteControl = addAdvancedPaletteControl ? resolveAdvancedPaletteConfig(addAdvancedPaletteControl) : void 0;
        }
        if (Object.prototype.hasOwnProperty.call(
          opts.config,
          "addMediaUploadControl"
        )) {
          nextConfig.addMediaUploadControl = addMediaUploadControl ? { ...addMediaUploadControl } : void 0;
        }
        return nextConfig;
      });
    }
    setSchema((prevSchema) => ({ ...prevSchema, ...newSchema }));
    setValues((prevValues) => {
      const updated = { ...prevValues };
      for (const key in newSchema) {
        const control = newSchema[key];
        if (!(key in updated)) {
          if ("value" in control) {
            updated[key] = control.value;
          }
        }
      }
      return updated;
    });
  };
  useEffect2(() => {
    if (!channelName) return;
    if (typeof window === "undefined") return;
    if (typeof window.BroadcastChannel === "undefined") return;
    const instanceId = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : Math.random().toString(36).slice(2);
    instanceIdRef.current = instanceId;
    const channel = new BroadcastChannel(channelName);
    channelRef.current = channel;
    const sendValues = () => {
      if (!instanceIdRef.current) return;
      channel.postMessage({
        type: "controls-sync-values",
        source: instanceIdRef.current,
        values: latestValuesRef.current
      });
    };
    const handleMessage = (event) => {
      const data = event.data;
      if (!data || data.source === instanceIdRef.current) return;
      if (data.type === "controls-sync-request") {
        sendValues();
        return;
      }
      if (data.type === "controls-sync-values" && data.values) {
        const incoming = data.values;
        setValues((prev) => {
          const prevKeys = Object.keys(prev);
          const incomingKeys = Object.keys(incoming);
          const sameLength = prevKeys.length === incomingKeys.length;
          const sameValues = sameLength && incomingKeys.every((key) => prev[key] === incoming[key]);
          if (sameValues) return prev;
          skipBroadcastRef.current = true;
          return { ...incoming };
        });
      }
    };
    channel.addEventListener("message", handleMessage);
    channel.postMessage({
      type: "controls-sync-request",
      source: instanceId
    });
    return () => {
      channel.removeEventListener("message", handleMessage);
      channel.close();
      channelRef.current = null;
      instanceIdRef.current = null;
    };
  }, [channelName]);
  useEffect2(() => {
    if (!channelRef.current || !instanceIdRef.current) return;
    if (skipBroadcastRef.current) {
      skipBroadcastRef.current = false;
      return;
    }
    channelRef.current.postMessage({
      type: "controls-sync-values",
      source: instanceIdRef.current,
      values
    });
  }, [values]);
  const contextValue = useMemo(
    () => ({
      schema,
      values,
      setValue,
      registerSchema,
      componentName,
      config
    }),
    [schema, values, componentName, config]
  );
  return /* @__PURE__ */ jsx2(ControlsContext.Provider, { value: contextValue, children });
};
var useControls = (schema, options) => {
  const ctx = useContext2(ControlsContext);
  if (!ctx) throw new Error("useControls must be used within ControlsProvider");
  const lastAdvancedPaletteSignature = useRef2(null);
  const urlParams = getUrlParams();
  const resolvedAdvancedConfig = options?.config?.addAdvancedPaletteControl ? resolveAdvancedPaletteConfig(options.config.addAdvancedPaletteControl) : void 0;
  const schemaWithAdvanced = useMemo(() => {
    const baseSchema = { ...schema };
    if (!resolvedAdvancedConfig) return baseSchema;
    return createAdvancedPaletteSchemaEntries(
      baseSchema,
      resolvedAdvancedConfig
    );
  }, [schema, resolvedAdvancedConfig]);
  const urlParamsKey = useMemo(() => JSON.stringify(urlParams), [urlParams]);
  const mergedSchema = useMemo(() => {
    return Object.fromEntries(
      Object.entries(schemaWithAdvanced).map(([key, control]) => {
        const urlValue = urlParams[key];
        if (!urlValue || !("value" in control)) return [key, control];
        const defaultValue = control.value;
        let parsed = urlValue;
        if (typeof defaultValue === "number") {
          parsed = parseFloat(urlValue);
          if (isNaN(parsed)) parsed = defaultValue;
        } else if (typeof defaultValue === "boolean") {
          parsed = urlValue === "true";
        }
        return [
          key,
          {
            ...control,
            value: parsed
          }
        ];
      })
    );
  }, [schemaWithAdvanced, urlParams, urlParamsKey]);
  useEffect2(() => {
    ctx.registerSchema(mergedSchema, options);
  }, [JSON.stringify(mergedSchema), JSON.stringify(options)]);
  useEffect2(() => {
    for (const key in mergedSchema) {
      if (!(key in ctx.values) && "value" in mergedSchema[key]) {
        ctx.setValue(key, mergedSchema[key].value);
      }
    }
  }, [JSON.stringify(mergedSchema), JSON.stringify(ctx.values)]);
  useEffect2(() => {
    if (!resolvedAdvancedConfig?.onPaletteChange) return;
    const palette = resolvedAdvancedConfig.sections.reduce(
      (acc, section) => {
        const channels = CHANNEL_KEYS.reduce(
          (channelAcc, channel) => {
            const key = createPaletteControlKey(
              resolvedAdvancedConfig.hiddenKeyPrefix,
              section.key,
              channel
            );
            const fallback = resolvedAdvancedConfig.defaultPalette?.[section.key]?.[channel] ?? 0;
            channelAcc[channel] = toNumberOr(ctx.values[key], fallback);
            return channelAcc;
          },
          {}
        );
        acc[section.key] = channels;
        return acc;
      },
      {}
    );
    const signature = createPaletteSignature(palette);
    if (lastAdvancedPaletteSignature.current === signature) return;
    lastAdvancedPaletteSignature.current = signature;
    resolvedAdvancedConfig.onPaletteChange(clonePalette(palette));
  }, [ctx.values, resolvedAdvancedConfig]);
  const typedValues = ctx.values;
  const jsx15 = useCallback(() => {
    if (!options?.componentName) return "";
    const props = Object.entries(typedValues).map(([key, val]) => {
      if (typeof val === "string") return `${key}="${val}"`;
      if (typeof val === "boolean") return `${key}={${val}}`;
      return `${key}={${JSON.stringify(val)}}`;
    }).join(" ");
    return `<${options.componentName} ${props} />`;
  }, [options?.componentName, JSON.stringify(typedValues)]);
  return {
    ...typedValues,
    controls: ctx.values,
    schema: ctx.schema,
    setValue: ctx.setValue,
    jsx: jsx15
  };
};
var useUrlSyncedControls = useControls;

// src/components/ControlPanel.tsx
import {
  useState as useState4,
  useMemo as useMemo4,
  useCallback as useCallback4,
  useEffect as useEffect6,
  useRef as useRef5
} from "react";
import {
  Check as Check2,
  Copy,
  SquareArrowOutUpRight,
  ChevronDown as ChevronDown2,
  Presentation
} from "lucide-react";

// src/hooks/usePreviewUrl.ts
import { useEffect as useEffect3, useState as useState3 } from "react";
var usePreviewUrl = (values, basePath = "") => {
  const [url, setUrl] = useState3("");
  useEffect3(() => {
    if (typeof window === "undefined") return;
    const params = new URLSearchParams();
    params.set(NO_CONTROLS_PARAM, "true");
    for (const [key, value] of Object.entries(values)) {
      if (value !== void 0 && value !== null) {
        params.set(key, value.toString());
      }
    }
    const fullUrl = `${basePath || window.location.pathname}?${params.toString()}`;
    setUrl(fullUrl);
  }, [values, basePath]);
  return url;
};

// src/components/ui/switch.tsx
import * as React3 from "react";
import * as SwitchPrimitives from "@radix-ui/react-switch";

// src/lib/utils.ts
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
function cn(...inputs) {
  return twMerge(clsx(inputs));
}

// src/components/ui/switch.tsx
import { jsx as jsx3 } from "react/jsx-runtime";
var Switch = React3.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx3(
  SwitchPrimitives.Root,
  {
    className: cn(
      "peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
      className
    ),
    ...props,
    ref,
    children: /* @__PURE__ */ jsx3(
      SwitchPrimitives.Thumb,
      {
        className: cn(
          "pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
        )
      }
    )
  }
));
Switch.displayName = SwitchPrimitives.Root.displayName;

// src/components/ui/label.tsx
import * as React4 from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva } from "class-variance-authority";
import { jsx as jsx4 } from "react/jsx-runtime";
var labelVariants = cva(
  "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
);
var Label = React4.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx4(
  LabelPrimitive.Root,
  {
    ref,
    className: cn(labelVariants(), className),
    ...props
  }
));
Label.displayName = LabelPrimitive.Root.displayName;

// src/components/ui/slider.tsx
import * as React5 from "react";
import * as SliderPrimitive from "@radix-ui/react-slider";
import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
var Slider = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsxs2(
  SliderPrimitive.Root,
  {
    ref,
    className: cn(
      "relative flex w-full touch-none select-none items-center",
      className
    ),
    ...props,
    children: [
      /* @__PURE__ */ jsx5(SliderPrimitive.Track, { className: "relative h-2 w-full grow overflow-hidden rounded-full bg-secondary", children: /* @__PURE__ */ jsx5(SliderPrimitive.Range, { className: "absolute h-full bg-primary" }) }),
      /* @__PURE__ */ jsx5(SliderPrimitive.Thumb, { className: "block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" })
    ]
  }
));
Slider.displayName = SliderPrimitive.Root.displayName;

// src/components/ui/input.tsx
import * as React6 from "react";
import { jsx as jsx6 } from "react/jsx-runtime";
var Input = React6.forwardRef(
  ({ className, type, ...props }, ref) => {
    return /* @__PURE__ */ jsx6(
      "input",
      {
        type,
        className: cn(
          "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
          className
        ),
        ref,
        ...props
      }
    );
  }
);
Input.displayName = "Input";

// src/components/ui/select.tsx
import * as React7 from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
import { jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
var Select = SelectPrimitive.Root;
var SelectValue = SelectPrimitive.Value;
var SelectTrigger = React7.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs3(
  SelectPrimitive.Trigger,
  {
    ref,
    className: cn(
      "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
      className
    ),
    ...props,
    children: [
      children,
      /* @__PURE__ */ jsx7(SelectPrimitive.Icon, { asChild: true, children: /* @__PURE__ */ jsx7(ChevronDown, { className: "h-4 w-4 opacity-50" }) })
    ]
  }
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
var SelectScrollUpButton = React7.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx7(
  SelectPrimitive.ScrollUpButton,
  {
    ref,
    className: cn(
      "flex cursor-default items-center justify-center py-1",
      className
    ),
    ...props,
    children: /* @__PURE__ */ jsx7(ChevronUp, { className: "h-4 w-4" })
  }
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
var SelectScrollDownButton = React7.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx7(
  SelectPrimitive.ScrollDownButton,
  {
    ref,
    className: cn(
      "flex cursor-default items-center justify-center py-1",
      className
    ),
    ...props,
    children: /* @__PURE__ */ jsx7(ChevronDown, { className: "h-4 w-4" })
  }
));
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
var SelectContent = React7.forwardRef(({ className, children, position = "popper", ...props }, ref) => /* @__PURE__ */ jsx7(SelectPrimitive.Portal, { children: /* @__PURE__ */ jsxs3(
  SelectPrimitive.Content,
  {
    ref,
    className: cn(
      "relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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 origin-[--radix-select-content-transform-origin]",
      position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
      className
    ),
    position,
    ...props,
    children: [
      /* @__PURE__ */ jsx7(SelectScrollUpButton, {}),
      /* @__PURE__ */ jsx7(
        SelectPrimitive.Viewport,
        {
          className: cn(
            "p-1",
            position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
          ),
          children
        }
      ),
      /* @__PURE__ */ jsx7(SelectScrollDownButton, {})
    ]
  }
) }));
SelectContent.displayName = SelectPrimitive.Content.displayName;
var SelectLabel = React7.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx7(
  SelectPrimitive.Label,
  {
    ref,
    className: cn("px-2 py-1.5 text-sm font-semibold", className),
    ...props
  }
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
var SelectItem = React7.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs3(
  SelectPrimitive.Item,
  {
    ref,
    className: cn(
      "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
      className
    ),
    ...props,
    children: [
      /* @__PURE__ */ jsx7("span", { className: "absolute right-2 flex h-3.5 w-3.5 items-center justify-center", children: /* @__PURE__ */ jsx7(SelectPrimitive.ItemIndicator, { children: /* @__PURE__ */ jsx7(Check, { className: "h-4 w-4" }) }) }),
      /* @__PURE__ */ jsx7(SelectPrimitive.ItemText, { children })
    ]
  }
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
var SelectSeparator = React7.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx7(
  SelectPrimitive.Separator,
  {
    ref,
    className: cn("-mx-1 my-1 h-px bg-muted", className),
    ...props
  }
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;

// src/components/ui/button.tsx
import * as React8 from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva as cva2 } from "class-variance-authority";
import { jsx as jsx8 } from "react/jsx-runtime";
var buttonVariants = cva2(
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
        destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
        outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
        secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
        ghost: "bg-gray-800 hover:bg-gray-900",
        link: "text-primary underline-offset-4 hover:underline"
      },
      size: {
        default: "h-9 px-4 py-2",
        sm: "h-8 rounded-md px-3 text-xs",
        lg: "h-10 rounded-md px-8",
        icon: "h-9 w-9"
      }
    },
    defaultVariants: {
      variant: "default",
      size: "default"
    }
  }
);
var Button = React8.forwardRef(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button";
    return /* @__PURE__ */ jsx8(
      Comp,
      {
        className: cn(buttonVariants({ variant, size, className })),
        ref,
        ...props
      }
    );
  }
);
Button.displayName = "Button";

// src/constants/layout.ts
var MOBILE_CONTROL_PANEL_PEEK = 112;

// src/components/AdvancedPaletteControl.tsx
import {
  useCallback as useCallback2,
  useEffect as useEffect4,
  useMemo as useMemo2,
  useRef as useRef3
} from "react";
import { jsx as jsx9, jsxs as jsxs4 } from "react/jsx-runtime";
var AdvancedPaletteControl = ({
  config
}) => {
  const { values, setValue } = useControlsContext();
  const palette = useMemo2(() => {
    const result = {};
    config.sections.forEach((section) => {
      result[section.key] = CHANNEL_KEYS.reduce((acc, channel) => {
        const key = createPaletteControlKey(
          config.hiddenKeyPrefix,
          section.key,
          channel
        );
        const defaultValue = config.defaultPalette?.[section.key]?.[channel] ?? DEFAULT_RANGES[section.key]?.min ?? 0;
        acc[channel] = toNumberOr(values?.[key], defaultValue);
        return acc;
      }, {});
    });
    return result;
  }, [config.defaultPalette, config.hiddenKeyPrefix, config.sections, values]);
  const paletteGradient = useMemo2(
    () => computePaletteGradient(palette, config.gradientSteps),
    [palette, config.gradientSteps]
  );
  const paletteSignature = useMemo2(
    () => createPaletteSignature(palette),
    [palette]
  );
  const lastSignatureRef = useRef3(null);
  useEffect4(() => {
    if (!config.onPaletteChange) return;
    if (lastSignatureRef.current === paletteSignature) return;
    lastSignatureRef.current = paletteSignature;
    config.onPaletteChange(palette);
  }, [config, palette, paletteSignature]);
  const updatePaletteValue = useCallback2(
    (sectionKey, channel, nextValue) => {
      const range = config.ranges[sectionKey] ?? DEFAULT_RANGES[sectionKey] ?? {
        min: 0,
        max: 1,
        step: 0.01
      };
      const clamped = Math.min(Math.max(nextValue, range.min), range.max);
      config.onInteraction?.();
      const controlKey = createPaletteControlKey(
        config.hiddenKeyPrefix,
        sectionKey,
        channel
      );
      setValue(controlKey, clamped);
    },
    [config, setValue]
  );
  const handleResetPalette = useCallback2(() => {
    config.onInteraction?.();
    config.sections.forEach((section) => {
      CHANNEL_KEYS.forEach((channel) => {
        const controlKey = createPaletteControlKey(
          config.hiddenKeyPrefix,
          section.key,
          channel
        );
        const defaultValue = config.defaultPalette?.[section.key]?.[channel] ?? DEFAULT_RANGES[section.key]?.min ?? 0;
        setValue(controlKey, defaultValue);
      });
    });
  }, [config, setValue]);
  return /* @__PURE__ */ jsx9("div", { className: "flex w-full flex-col gap-6", children: /* @__PURE__ */ jsxs4("div", { className: "flex w-full flex-col gap-4", children: [
    /* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-between", children: [
      /* @__PURE__ */ jsx9("span", { className: "text-xs font-semibold uppercase tracking-wide text-stone-200", children: "Palette" }),
      /* @__PURE__ */ jsx9(
        "button",
        {
          type: "button",
          onClick: handleResetPalette,
          className: "rounded border border-stone-700 px-3 py-1 text-[10px] font-semibold uppercase tracking-widest text-stone-200 transition hover:border-stone-500",
          children: "Reset Palette"
        }
      )
    ] }),
    /* @__PURE__ */ jsx9(
      "div",
      {
        className: "h-4 w-full rounded border border-stone-700",
        style: { background: paletteGradient }
      }
    ),
    /* @__PURE__ */ jsx9("div", { className: "flex flex-col gap-4", children: config.sections.map((section) => {
      const range = config.ranges[section.key];
      return /* @__PURE__ */ jsxs4("div", { className: "space-y-3", children: [
        /* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-between text-[11px] uppercase tracking-widest text-stone-300", children: [
          /* @__PURE__ */ jsx9("span", { children: section.label }),
          section.helper && /* @__PURE__ */ jsx9("span", { className: "text-stone-500", children: section.helper })
        ] }),
        /* @__PURE__ */ jsx9("div", { className: "grid grid-cols-3 gap-3", children: CHANNEL_KEYS.map((channel) => {
          const value = palette[section.key][channel];
          const channelLabel = config.channelLabels?.[channel] ?? DEFAULT_CHANNEL_LABELS[channel];
          return /* @__PURE__ */ jsxs4("div", { className: "space-y-2", children: [
            /* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-between text-[10px] uppercase tracking-widest text-stone-400", children: [
              /* @__PURE__ */ jsx9("span", { children: channelLabel }),
              /* @__PURE__ */ jsx9("span", { children: value.toFixed(2) })
            ] }),
            /* @__PURE__ */ jsx9(
              "input",
              {
                type: "range",
                min: range.min,
                max: range.max,
                step: range.step,
                value,
                onPointerDown: config.onInteraction,
                onChange: (event) => updatePaletteValue(
                  section.key,
                  channel,
                  parseFloat(event.target.value)
                ),
                className: "w-full cursor-pointer accent-stone-300"
              }
            ),
            /* @__PURE__ */ jsx9(
              "input",
              {
                type: "number",
                min: range.min,
                max: range.max,
                step: range.step,
                value: value.toFixed(3),
                onPointerDown: config.onInteraction,
                onFocus: config.onInteraction,
                onChange: (event) => {
                  const parsed = parseFloat(event.target.value);
                  if (Number.isNaN(parsed)) return;
                  updatePaletteValue(section.key, channel, parsed);
                },
                className: "w-full rounded border border-stone-700 bg-stone-900 px-2 py-1 text-xs text-stone-200 focus:border-stone-500 focus:outline-none"
              }
            )
          ] }, channel);
        }) })
      ] }, section.key);
    }) })
  ] }) });
};
var AdvancedPaletteControl_default = AdvancedPaletteControl;

// src/components/MediaUploadControl.tsx
import {
  useCallback as useCallback3,
  useEffect as useEffect5,
  useId,
  useMemo as useMemo3,
  useRef as useRef4,
  useSyncExternalStore
} from "react";
import { X } from "lucide-react";

// src/state/mediaSelectionStore.ts
var snapshot = {
  media: null,
  error: null
};
var listeners = /* @__PURE__ */ new Set();
var emitChange = () => {
  for (const listener of listeners) {
    listener();
  }
};
var mediaSelectionStore = {
  subscribe: (listener) => {
    listeners.add(listener);
    return () => {
      listeners.delete(listener);
    };
  },
  getSnapshot: () => snapshot,
  setSnapshot: (next) => {
    snapshot = next;
    emitChange();
  }
};

// src/components/MediaUploadControl.tsx
import { jsx as jsx10, jsxs as jsxs5 } from "react/jsx-runtime";
var DEFAULT_PRESET_MEDIA = [
  {
    src: "/v0.png",
    label: "Default",
    type: "image"
  },
  {
    src: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?w=240&auto=format&fit=crop",
    label: "Mountains",
    type: "image"
  },
  {
    src: "https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=240&auto=format&fit=crop",
    label: "Beach",
    type: "image"
  },
  {
    src: "https://images.unsplash.com/photo-1518770660439-4636190af475?w=240&auto=format&fit=crop",
    label: "City",
    type: "image"
  }
];
function MediaUploadControl({
  onSelectMedia,
  onClear,
  presetMedia,
  maxPresetCount
}) {
  const inputId = useId();
  const inputRef = useRef4(null);
  const uploadedUrlRef = useRef4(null);
  const { media, error } = useSyncExternalStore(
    mediaSelectionStore.subscribe,
    mediaSelectionStore.getSnapshot,
    mediaSelectionStore.getSnapshot
  );
  const VIDEO_EXTENSIONS = useMemo3(
    () => [".mp4", ".webm", ".ogg", ".ogv", ".mov", ".m4v"],
    []
  );
  const setSelection = useCallback3(
    (next) => {
      mediaSelectionStore.setSnapshot(next);
    },
    []
  );
  const handleFileChange = (event) => {
    const file = event.target.files?.[0];
    if (!file) {
      return;
    }
    if (uploadedUrlRef.current) {
      URL.revokeObjectURL(uploadedUrlRef.current);
      uploadedUrlRef.current = null;
    }
    const objectUrl = URL.createObjectURL(file);
    uploadedUrlRef.current = objectUrl;
    const lowerName = file.name?.toLowerCase() ?? "";
    const hasVideoExtension = VIDEO_EXTENSIONS.some(
      (ext) => lowerName.endsWith(ext)
    );
    const isVideo = file.type.startsWith("video/") || hasVideoExtension;
    if (isVideo) {
      setSelection({
        media: null,
        error: "Videos are not supported in this effect yet."
      });
      return;
    }
    const nextMedia = { src: objectUrl, type: "image" };
    setSelection({ media: nextMedia, error: null });
    onSelectMedia(nextMedia);
  };
  const handleClearSelection = () => {
    if (uploadedUrlRef.current) {
      URL.revokeObjectURL(uploadedUrlRef.current);
      uploadedUrlRef.current = null;
    }
    setSelection({ media: null, error: null });
    onClear();
  };
  const handlePresetSelect = (entry) => {
    if (entry.type === "video") {
      setSelection({
        media: null,
        error: "Videos are not supported in this effect yet."
      });
      return;
    }
    const nextMedia = { src: entry.src, type: entry.type };
    setSelection({ media: nextMedia, error: null });
    onSelectMedia(nextMedia);
  };
  useEffect5(() => {
    return () => {
      if (uploadedUrlRef.current) {
        URL.revokeObjectURL(uploadedUrlRef.current);
        uploadedUrlRef.current = null;
      }
    };
  }, []);
  const presets = useMemo3(() => {
    const source = presetMedia ?? DEFAULT_PRESET_MEDIA;
    if (typeof maxPresetCount === "number" && Number.isFinite(maxPresetCount)) {
      const safeCount = Math.max(0, Math.floor(maxPresetCount));
      return source.slice(0, safeCount);
    }
    return source;
  }, [presetMedia, maxPresetCount]);
  return /* @__PURE__ */ jsxs5(
    "div",
    {
      style: {
        display: "flex",
        flexDirection: "column",
        gap: "0.5rem"
      },
      children: [
        /* @__PURE__ */ jsx10("label", { htmlFor: inputId, style: { fontSize: "0.85rem", fontWeight: 500 }, children: "Upload media" }),
        /* @__PURE__ */ jsx10(
          "input",
          {
            id: inputId,
            type: "file",
            accept: "image/*",
            ref: inputRef,
            style: { display: "none" },
            onChange: handleFileChange
          }
        ),
        /* @__PURE__ */ jsxs5(
          "div",
          {
            style: {
              display: "flex",
              alignItems: "center",
              gap: "0.75rem"
            },
            children: [
              /* @__PURE__ */ jsx10(
                "button",
                {
                  type: "button",
                  onClick: () => inputRef.current?.click(),
                  style: {
                    padding: "0.35rem 0.75rem",
                    borderRadius: "0.4rem",
                    border: "1px solid rgba(255, 255, 255, 0.25)",
                    background: "rgba(255, 255, 255, 0.08)",
                    color: "inherit",
                    cursor: "pointer"
                  },
                  children: "Choose file"
                }
              ),
              media ? /* @__PURE__ */ jsx10(
                "div",
                {
                  style: {
                    width: 36,
                    height: 36,
                    borderRadius: "0.35rem",
                    overflow: "hidden",
                    border: "1px solid rgba(255, 255, 255, 0.15)"
                  },
                  children: /* @__PURE__ */ jsx10(
                    "img",
                    {
                      src: media.src,
                      alt: "Thumbnail",
                      style: {
                        width: "100%",
                        height: "100%",
                        objectFit: "cover",
                        display: "block"
                      }
                    }
                  )
                }
              ) : null,
              media ? /* @__PURE__ */ jsx10(
                "button",
                {
                  type: "button",
                  onClick: handleClearSelection,
                  style: {
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "center",
                    padding: "0.3rem",
                    borderRadius: "0.4rem",
                    border: "1px solid rgba(255,255,255,0.2)",
                    background: "transparent",
                    color: "inherit",
                    cursor: "pointer"
                  },
                  "aria-label": "Clear selection",
                  title: "Clear selection",
                  children: /* @__PURE__ */ jsx10(X, { size: 16, strokeWidth: 2 })
                }
              ) : null
            ]
          }
        ),
        presets.length > 0 ? /* @__PURE__ */ jsx10(
          "div",
          {
            style: {
              display: "grid",
              gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
              gap: "0.5rem"
            },
            children: presets.map((entry) => {
              const isSelected = media?.src === entry.src && media?.type === entry.type;
              return /* @__PURE__ */ jsxs5(
                "button",
                {
                  type: "button",
                  onClick: () => handlePresetSelect(entry),
                  style: {
                    width: "100%",
                    borderRadius: "0.4rem",
                    border: "1px solid rgba(255,255,255,0.25)",
                    outline: isSelected ? "2px solid #fff" : "none",
                    outlineOffset: 2,
                    padding: 0,
                    overflow: "hidden",
                    background: "transparent",
                    cursor: "pointer"
                  },
                  children: [
                    /* @__PURE__ */ jsx10(
                      "img",
                      {
                        src: entry.src,
                        alt: entry.label,
                        style: {
                          width: "100%",
                          height: 100,
                          objectFit: "cover",
                          display: "block"
                        }
                      }
                    ),
                    /* @__PURE__ */ jsx10(
                      "span",
                      {
                        style: {
                          display: "block",
                          padding: "0.35rem",
                          fontSize: "0.75rem",
                          textAlign: "left",
                          background: "rgba(0,0,0,0.45)"
                        },
                        children: entry.label
                      }
                    )
                  ]
                },
                `${entry.src}-${entry.type}`
              );
            })
          }
        ) : null,
        error ? /* @__PURE__ */ jsx10("p", { style: { color: "#ff9da4", fontSize: "0.8rem" }, children: error }) : null
      ]
    }
  );
}

// src/components/ControlPanel.tsx
import { Fragment, jsx as jsx11, jsxs as jsxs6 } from "react/jsx-runtime";
var splitPropsString = (input) => {
  const props = [];
  let current = "";
  let curlyDepth = 0;
  let squareDepth = 0;
  let parenDepth = 0;
  let inSingleQuote = false;
  let inDoubleQuote = false;
  let inBacktick = false;
  let escapeNext = false;
  for (const char of input) {
    if (escapeNext) {
      current += char;
      escapeNext = false;
      continue;
    }
    if (char === "\\") {
      current += char;
      escapeNext = true;
      continue;
    }
    if (char === "'" && !inDoubleQuote && !inBacktick) {
      inSingleQuote = !inSingleQuote;
      current += char;
      continue;
    }
    if (char === '"' && !inSingleQuote && !inBacktick) {
      inDoubleQuote = !inDoubleQuote;
      current += char;
      continue;
    }
    if (char === "`" && !inSingleQuote && !inDoubleQuote) {
      inBacktick = !inBacktick;
      current += char;
      continue;
    }
    if (!inSingleQuote && !inDoubleQuote && !inBacktick) {
      if (char === "{") {
        curlyDepth += 1;
      } else if (char === "}") {
        curlyDepth = Math.max(0, curlyDepth - 1);
      } else if (char === "[") {
        squareDepth += 1;
      } else if (char === "]") {
        squareDepth = Math.max(0, squareDepth - 1);
      } else if (char === "(") {
        parenDepth += 1;
      } else if (char === ")") {
        parenDepth = Math.max(0, parenDepth - 1);
      }
    }
    const atTopLevel = !inSingleQuote && !inDoubleQuote && !inBacktick && curlyDepth === 0 && squareDepth === 0 && parenDepth === 0;
    if (atTopLevel && /\s/.test(char)) {
      if (current.trim()) {
        props.push(current.trim());
      }
      current = "";
      continue;
    }
    current += char;
  }
  if (current.trim()) {
    props.push(current.trim());
  }
  return props;
};
var formatJsxCodeSnippet = (input) => {
  const trimmed = input.trim();
  if (!trimmed) return "";
  if (trimmed.includes("\n")) {
    return trimmed;
  }
  if (!trimmed.startsWith("<") || !trimmed.endsWith(">")) {
    return trimmed;
  }
  if (!trimmed.endsWith("/>")) {
    return trimmed;
  }
  const inner = trimmed.slice(1, -2).trim();
  const firstSpaceIndex = inner.indexOf(" ");
  if (firstSpaceIndex === -1) {
    return `<${inner} />`;
  }
  const componentName = inner.slice(0, firstSpaceIndex);
  const propsString = inner.slice(firstSpaceIndex + 1).trim();
  if (!propsString) {
    return `<${componentName} />`;
  }
  const propsList = splitPropsString(propsString);
  if (propsList.length === 0) {
    return `<${componentName} ${propsString} />`;
  }
  const formattedProps = propsList.map((prop) => `  ${prop}`).join("\n");
  return `<${componentName}
${formattedProps}
/>`;
};
var isWhitespace = (char) => /\s/.test(char);
var isAttrNameChar = (char) => /[A-Za-z0-9_$\-.:]/.test(char);
var isAlphaStart = (char) => /[A-Za-z_$]/.test(char);
var tokenizeJsx = (input) => {
  const tokens = [];
  let i = 0;
  while (i < input.length) {
    const char = input[i];
    if (char === "<") {
      tokens.push({ type: "punctuation", value: "<" });
      i += 1;
      if (input[i] === "/") {
        tokens.push({ type: "punctuation", value: "/" });
        i += 1;
      }
      const start = i;
      while (i < input.length && isAttrNameChar(input[i])) {
        i += 1;
      }
      if (i > start) {
        tokens.push({ type: "tag", value: input.slice(start, i) });
      }
      continue;
    }
    if (char === "/" && input[i + 1] === ">") {
      tokens.push({ type: "punctuation", value: "/>" });
      i += 2;
      continue;
    }
    if (char === ">") {
      tokens.push({ type: "punctuation", value: ">" });
      i += 1;
      continue;
    }
    if (char === "=") {
      tokens.push({ type: "punctuation", value: "=" });
      i += 1;
      continue;
    }
    if (char === '"' || char === "'" || char === "`") {
      const quote = char;
      let j = i + 1;
      let value = quote;
      while (j < input.length) {
        const current = input[j];
        value += current;
        if (current === quote && input[j - 1] !== "\\") {
          break;
        }
        j += 1;
      }
      tokens.push({ type: "string", value });
      i = j + 1;
      continue;
    }
    if (char === "{") {
      let depth = 1;
      let j = i + 1;
      while (j < input.length && depth > 0) {
        if (input[j] === "{") {
          depth += 1;
        } else if (input[j] === "}") {
          depth -= 1;
        }
        j += 1;
      }
      const expression = input.slice(i, j);
      tokens.push({ type: "expression", value: expression });
      i = j;
      continue;
    }
    if (isAlphaStart(char)) {
      const start = i;
      i += 1;
      while (i < input.length && isAttrNameChar(input[i])) {
        i += 1;
      }
      const word = input.slice(start, i);
      let k = i;
      while (k < input.length && isWhitespace(input[k])) {
        k += 1;
      }
      if (input[k] === "=") {
        tokens.push({ type: "attrName", value: word });
      } else {
        tokens.push({ type: "plain", value: word });
      }
      continue;
    }
    tokens.push({ type: "plain", value: char });
    i += 1;
  }
  return tokens;
};
var TOKEN_CLASS_MAP = {
  tag: "text-sky-300",
  attrName: "text-amber-200",
  string: "text-emerald-300",
  expression: "text-purple-300",
  punctuation: "text-stone-400"
};
var highlightJsx = (input) => {
  const tokens = tokenizeJsx(input);
  const nodes = [];
  tokens.forEach((token, index) => {
    if (token.type === "plain") {
      nodes.push(token.value);
    } else {
      nodes.push(
        /* @__PURE__ */ jsx11("span", { className: TOKEN_CLASS_MAP[token.type], children: token.value }, `token-${index}`)
      );
    }
  });
  return nodes;
};
var ControlPanel = () => {
  const [copied, setCopied] = useState4(false);
  const [codeCopied, setCodeCopied] = useState4(false);
  const [isCodeVisible, setIsCodeVisible] = useState4(false);
  const [folderStates, setFolderStates] = useState4({});
  const codeCopyTimeoutRef = useRef5(null);
  const { leftPanelWidth, isDesktop, isHydrated } = useResizableLayout();
  const { schema, setValue, values, componentName, config } = useControlsContext();
  const isControlsOnlyView = typeof window !== "undefined" && new URLSearchParams(window.location.search).get(CONTROLS_ONLY_PARAM) === "true";
  const previewUrl = usePreviewUrl(values);
  const buildUrl = useCallback4(
    (modifier) => {
      if (!previewUrl) return "";
      const [path, search = ""] = previewUrl.split("?");
      const params = new URLSearchParams(search);
      modifier(params);
      const query = params.toString();
      return query ? `${path}?${query}` : path;
    },
    [previewUrl]
  );
  const presentationUrl = useMemo4(() => {
    if (!previewUrl) return "";
    return buildUrl((params) => {
      params.set(PRESENTATION_PARAM, "true");
    });
  }, [buildUrl, previewUrl]);
  const controlsOnlyUrl = useMemo4(() => {
    if (!previewUrl) return "";
    return buildUrl((params) => {
      params.delete(NO_CONTROLS_PARAM);
      params.delete(PRESENTATION_PARAM);
      params.set(CONTROLS_ONLY_PARAM, "true");
    });
  }, [buildUrl, previewUrl]);
  const handlePresentationClick = useCallback4(() => {
    if (typeof window === "undefined" || !presentationUrl) return;
    window.open(presentationUrl, "_blank", "noopener,noreferrer");
    if (controlsOnlyUrl) {
      const viewportWidth = window.innerWidth || 1200;
      const viewportHeight = window.innerHeight || 900;
      const controlsWidth = Math.max(
        320,
        Math.min(600, Math.round(viewportWidth * leftPanelWidth / 100))
      );
      const controlsHeight = Math.max(600, viewportHeight);
      const controlsFeatures = [
        "noopener",
        "noreferrer",
        "toolbar=0",
        "menubar=0",
        "resizable=yes",
        "scrollbars=yes",
        `width=${controlsWidth}`,
        `height=${controlsHeight}`
      ].join(",");
      window.open(controlsOnlyUrl, "v0-controls", controlsFeatures);
    }
  }, [controlsOnlyUrl, leftPanelWidth, presentationUrl]);
  const jsx15 = useMemo4(() => {
    if (!componentName) return "";
    const props = Object.entries(values).map(([key, val]) => {
      if (typeof val === "string") return `${key}="${val}"`;
      if (typeof val === "boolean") return `${key}={${val}}`;
      return `${key}={${JSON.stringify(val)}}`;
    }).join(" ");
    return `<${componentName} ${props} />`;
  }, [componentName, values]);
  const visibleEntries = Object.entries(schema).filter(
    ([, control]) => !control.hidden
  );
  const rootControls = [];
  const folderOrder = [];
  const folderControls = /* @__PURE__ */ new Map();
  const folderExtras = /* @__PURE__ */ new Map();
  const folderPlacement = /* @__PURE__ */ new Map();
  const seenFolders = /* @__PURE__ */ new Set();
  const ensureFolder = (folder) => {
    if (!seenFolders.has(folder)) {
      seenFolders.add(folder);
      folderOrder.push(folder);
    }
  };
  visibleEntries.forEach((entry) => {
    const [key, control] = entry;
    const folder = control.folder?.trim();
    if (folder) {
      const placement = control.folderPlacement ?? "bottom";
      ensureFolder(folder);
      if (!folderControls.has(folder)) {
        folderControls.set(folder, []);
      }
      folderControls.get(folder).push(entry);
      const existingPlacement = folderPlacement.get(folder);
      if (!existingPlacement || placement === "top") {
        folderPlacement.set(folder, placement);
      }
    } else {
      rootControls.push(entry);
    }
  });
  const advancedConfig = config?.addAdvancedPaletteControl;
  let advancedPaletteControlNode = null;
  if (advancedConfig) {
    const advancedNode = /* @__PURE__ */ jsx11(
      AdvancedPaletteControl_default,
      {
        config: advancedConfig
      },
      "advancedPaletteControl"
    );
    const advancedFolder = advancedConfig.folder?.trim();
    if (advancedFolder) {
      const placement = advancedConfig.folderPlacement ?? "bottom";
      ensureFolder(advancedFolder);
      if (!folderControls.has(advancedFolder)) {
        folderControls.set(advancedFolder, []);
      }
      const existingPlacement = folderPlacement.get(advancedFolder);
      if (!existingPlacement || placement === "top") {
        folderPlacement.set(advancedFolder, placement);
      }
      if (!folderExtras.has(advancedFolder)) {
        folderExtras.set(advancedFolder, []);
      }
      folderExtras.get(advancedFolder).push(advancedNode);
    } else {
      advancedPaletteControlNode = advancedNode;
    }
  }
  const mediaUploadConfig = config?.addMediaUploadControl;
  let mediaUploadControlNode = null;
  if (mediaUploadConfig) {
    const mediaUploadNode = /* @__PURE__ */ jsx11(
      MediaUploadControl,
      {
        onSelectMedia: (media) => {
          mediaUploadConfig.onSelectMedia?.(media);
        },
        onClear: () => {
          mediaUploadConfig.onClear?.();
        },
        presetMedia: mediaUploadConfig.presetMedia,
        maxPresetCount: mediaUploadConfig.maxPresetCount
      },
      "mediaUploadControl"
    );
    const mediaFolder = mediaUploadConfig.folder?.trim();
    if (mediaFolder) {
      const placement = mediaUploadConfig.folderPlacement ?? "bottom";
      ensureFolder(mediaFolder);
      if (!folderControls.has(mediaFolder)) {
        folderControls.set(mediaFolder, []);
      }
      const existingPlacement = folderPlacement.get(mediaFolder);
      if (!existingPlacement || placement === "top") {
        folderPlacement.set(mediaFolder, placement);
      }
      if (!folderExtras.has(mediaFolder)) {
        folderExtras.set(mediaFolder, []);
      }
      folderExtras.get(mediaFolder).push(mediaUploadNode);
    } else {
      mediaUploadControlNode = mediaUploadNode;
    }
  }
  const rootButtonControls = [];
  const rootNormalControls = [];
  rootControls.forEach((entry) => {
    const [key, control] = entry;
    if (control.type === "button") {
      rootButtonControls.push([key, control]);
    } else {
      rootNormalControls.push(entry);
    }
  });
  const folderGroups = folderOrder.map((folder) => ({
    folder,
    entries: folderControls.get(folder) ?? [],
    extras: folderExtras.get(folder) ?? [],
    placement: folderPlacement.get(folder) ?? "bottom"
  })).filter((group) => group.entries.length > 0 || group.extras.length > 0);
  const hasRootButtonControls = rootButtonControls.length > 0;
  const hasAnyFolders = folderGroups.length > 0;
  const jsonToComponentString = useCallback4(
    ({
      componentName: componentNameOverride,
      props
    }) => {
      const resolvedComponentName = componentNameOverride ?? componentName;
      if (!resolvedComponentName) return "";
      const formatProp = (key, value) => {
        if (value === void 0) return null;
        if (value === null) return `${key}={null}`;
        if (typeof value === "string") return `${key}="${value}"`;
        if (typeof value === "number" || typeof value === "boolean") {
          return `${key}={${value}}`;
        }
        if (typeof value === "bigint") {
          return `${key}={${value.toString()}n}`;
        }
        return `${key}={${JSON.stringify(value)}}`;
      };
      const formattedProps = Object.entries(props ?? {}).map(([key, value]) => formatProp(key, value)).filter((prop) => Boolean(prop)).join(" ");
      if (!formattedProps) {
        return `<${resolvedComponentName} />`;
      }
      return `<${resolvedComponentName} ${formattedProps} />`;
    },
    [componentName]
  );
  const copyText = config?.showCopyButtonFn?.({
    componentName,
    values,
    schema,
    jsx: jsx15,
    jsonToComponentString
  }) ?? jsx15;
  const shouldShowCopyButton = config?.showCopyButton !== false && Boolean(copyText);
  const baseSnippet = copyText || jsx15;
  const formattedCode = useMemo4(
    () => formatJsxCodeSnippet(baseSnippet),
    [baseSnippet]
  );
  const hasCodeSnippet = Boolean(config?.showCodeSnippet && formattedCode);
  const highlightedCode = useMemo4(
    () => formattedCode ? highlightJsx(formattedCode) : null,
    [formattedCode]
  );
  useEffect6(() => {
    if (!hasCodeSnippet) {
      setIsCodeVisible(false);
    }
  }, [hasCodeSnippet]);
  useEffect6(() => {
    setCodeCopied(false);
    if (codeCopyTimeoutRef.current) {
      clearTimeout(codeCopyTimeoutRef.current);
      codeCopyTimeoutRef.current = null;
    }
  }, [formattedCode]);
  useEffect6(() => {
    return () => {
      if (codeCopyTimeoutRef.current) {
        clearTimeout(codeCopyTimeoutRef.current);
      }
    };
  }, []);
  const handleToggleCodeVisibility = useCallback4(() => {
    setIsCodeVisible((prev) => {
      const next = !prev;
      if (!next) {
        setCodeCopied(false);
        if (codeCopyTimeoutRef.current) {
          clearTimeout(codeCopyTimeoutRef.current);
          codeCopyTimeoutRef.current = null;
        }
      }
      return next;
    });
  }, []);
  const handleCodeCopy = useCallback4(() => {
    if (!formattedCode) return;
    if (typeof navigator === "undefined" || !navigator.clipboard || typeof navigator.clipboard.writeText !== "function") {
      return;
    }
    navigator.clipboard.writeText(formattedCode).then(() => {
      setCodeCopied(true);
      if (codeCopyTimeoutRef.current) {
        clearTimeout(codeCopyTimeoutRef.current);
      }
      codeCopyTimeoutRef.current = setTimeout(() => {
        setCodeCopied(false);
        codeCopyTimeoutRef.current = null;
      }, 3e3);
    }).catch(() => {
    });
  }, [formattedCode]);
  const labelize = (key) => key.replace(/([A-Z])/g, " $1").replace(/[\-_]/g, " ").replace(/\s+/g, " ").trim().replace(/(^|\s)\S/g, (s) => s.toUpperCase());
  const renderButtonControl = (key, control, variant) => /* @__PURE__ */ jsx11(
    "div",
    {
      className: variant === "root" ? "flex-1 [&_[data-slot=button]]:w-full" : "[&_[data-slot=button]]:w-full",
      children: control.render ? control.render() : /* @__PURE__ */ jsx11(
        "button",
        {
          onClick: control.onClick,
          className: "w-full px-4 py-2 text-sm bg-stone-800 hover:bg-stone-700 text-white rounded-md shadow",
          children: control.label ?? key
        }
      )
    },
    `control-panel-custom-${key}`
  );
  const renderControl = (key, control, variant) => {
    if (control.type === "button") {
      return renderButtonControl(key, control, variant);
    }
    const value = values[key];
    switch (control.type) {
      case "boolean":
        return /* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-between", children: [
          /* @__PURE__ */ jsx11(Label, { htmlFor: key, className: "cursor-pointer", children: labelize(key) }),
          /* @__PURE__ */ jsx11(
            Switch,
            {
              id: key,
              checked: value,
              onCheckedChange: (v) => setValue(key, v),
              className: "cursor-pointer scale-90"
            }
          )
        ] }, key);
      case "number":
        return /* @__PURE__ */ jsxs6("div", { className: "space-y-3 w-full", children: [
          /* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-between", children: [
            /* @__PURE__ */ jsx11(Label, { className: "text-stone-300", htmlFor: key, children: labelize(key) }),
            /* @__PURE__ */ jsx11(
              Input,
              {
                type: "number",
                value,
                min: control.min ?? 0,
                max: control.max ?? 100,
                step: control.step ?? 1,
                onChange: (e) => {
                  const v = parseFloat(e.target.value);
                  if (Number.isNaN(v)) return;
                  setValue(key, v);
                },
                className: "w-20 text-center cursor-text"
              }
            )
          ] }),
          /* @__PURE__ */ jsx11(
            Slider,
            {
              id: key,
              min: control.min ?? 0,
              max: control.max ?? 100,
              step: control.step ?? 1,
              value: [value],
              onValueChange: ([v]) => setValue(key, v),
              className: "w-full cursor-pointer"
            }
          )
        ] }, key);
      case "string":
        return /* @__PURE__ */ jsxs6("div", { className: "space-y-2 w-full", children: [
          /* @__PURE__ */ jsx11(Label, { className: "text-stone-300", htmlFor: key, children: labelize(key) }),
          /* @__PURE__ */ jsx11(
            Input,
            {
              id: key,
              value,
              placeholder: key,
              onChange: (e) => setValue(key, e.target.value),
              className: "bg-stone-900"
            }
          )
        ] }, key);
      case "color":
        return /* @__PURE__ */ jsxs6("div", { className: "space-y-2 w-full", children: [
          /* @__PURE__ */ jsx11(Label, { className: "text-stone-300", htmlFor: key, children: labelize(key) }),
          /* @__PURE__ */ jsx11(
            "input",
            {
              type: "color",
              id: key,
              value,
              onChange: (e) => setValue(key, e.target.value),
              className: "w-full h-10 rounded border border-stone-600 bg-transparent"
            }
          )
        ] }, key);
      case "select":
        return /* @__PURE__ */ jsx11("div", { className: "space-y-2", children: /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-3", children: [
          /* @__PURE__ */ jsx11(Label, { className: "min-w-fit", htmlFor: key, children: labelize(key) }),
          /* @__PURE__ */ jsxs6(Select, { value, onValueChange: (val) => setValue(key, val), children: [
            /* @__PURE__ */ jsx11(SelectTrigger, { className: "flex-1 cursor-pointer", children: /* @__PURE__ */ jsx11(SelectValue, { placeholder: "Select option" }) }),
            /* @__PURE__ */ jsx11(SelectContent, { className: "cursor-pointer z-[9999]", children: Object.entries(control.options).map(([label]) => /* @__PURE__ */ jsx11(
              SelectItem,
              {
                value: label,
                className: "cursor-pointer",
                children: label
              },
              label
            )) })
          ] })
        ] }) }, key);
      default:
        return null;
    }
  };
  const renderFolder = (folder, entries, extras = []) => {
    const isOpen = folderStates[folder] ?? true;
    return /* @__PURE__ */ jsxs6(
      "div",
      {
        className: "border border-stone-700/60 rounded-lg bg-stone-900/70",
        children: [
          /* @__PURE__ */ jsxs6(
            "button",
            {
              type: "button",
              onClick: () => setFolderStates((prev) => ({
                ...prev,
                [folder]: !isOpen
              })),
              className: "w-full flex items-center justify-between px-4 py-3 text-left font-semibold text-stone-200 tracking-wide",
              children: [
                /* @__PURE__ */ jsx11("span", { children: folder }),
                /* @__PURE__ */ jsx11(
                  ChevronDown2,
                  {
                    className: `w-4 h-4 transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`
                  }
                )
              ]
            }
          ),
          isOpen && /* @__PURE__ */ jsxs6("div", { className: "px-4 pb-4 pt-0 space-y-5", children: [
            entries.map(
              ([key, control]) => renderControl(key, control, "folder")
            ),
            extras.map((extra) => extra)
          ] })
        ]
      },
      folder
    );
  };
  const topFolderSections = hasAnyFolders ? folderGroups.filter(({ placement }) => placement === "top").map(
    ({ folder, entries, extras }) => renderFolder(folder, entries, extras)
  ) : null;
  const bottomFolderSections = hasAnyFolders ? folderGroups.filter(({ placement }) => placement === "bottom").map(
    ({ folder, entries, extras }) => renderFolder(folder, entries, extras)
  ) : null;
  const panelStyle = {
    width: "100%",
    height: "auto",
    flex: "0 0 auto"
  };
  if (isHydrated && !isControlsOnlyView) {
    if (isDesktop) {
      Object.assign(panelStyle, {
        position: "absolute",
        left: 0,
        top: 0,
        bottom: 0,
        width: `${leftPanelWidth}%`,
        overflowY: "auto"
      });
    } else {
      Object.assign(panelStyle, {
        marginTop: `calc(-1 * (${MOBILE_CONTROL_PANEL_PEEK}px + env(safe-area-inset-bottom, 0px)))`,
        paddingBottom: `calc(${MOBILE_CONTROL_PANEL_PEEK}px + env(safe-area-inset-bottom, 0px))`
      });
    }
  }
  return /* @__PURE__ */ jsx11(
    "div",
    {
      className: `order-2 md:order-1 w-full md:h-auto p-2 md:p-4 bg-stone-900 font-mono text-stone-300 transition-opacity duration-300 z-max ${!isHydrated ? "opacity-0" : "opacity-100"}`,
      onPointerDown: (e) => e.stopPropagation(),
      onTouchStart: (e) => e.stopPropagation(),
      style: panelStyle,
      children: /* @__PURE__ */ jsxs6("div", { className: "dark mb-10 space-y-6 p-4 md:p-6 bg-stone-900/95 backdrop-blur-md border-2 border-stone-700 rounded-xl shadow-lg", children: [
        /* @__PURE__ */ jsx11("div", { className: "space-y-1", children: /* @__PURE__ */ jsx11("h1", { className: "text-lg text-stone-100 font-semibold", children: config?.mainLabel ?? "Controls" }) }),
        /* @__PURE__ */ jsxs6("div", { className: "space-y-6", children: [
          topFolderSections,
          hasRootButtonControls && /* @__PURE__ */ jsx11("div", { className: "flex flex-wrap gap-2", children: rootButtonControls.map(
            ([key, control]) => renderButtonControl(key, control, "root")
          ) }),
          advancedPaletteControlNode,
          mediaUploadControlNode,
          rootNormalControls.map(
            ([key, control]) => renderControl(key, control, "root")
          ),
          bottomFolderSections,
          hasCodeSnippet && /* @__PURE__ */ jsxs6("div", { className: "border border-stone-700/60 rounded-lg bg-stone-900/70", children: [
            /* @__PURE__ */ jsxs6(
              "button",
              {
                type: "button",
                onClick: handleToggleCodeVisibility,
                className: "w-full flex items-center justify-between px-4 py-3 text-left font-semibold text-stone-200 tracking-wide",
                "aria-expanded": isCodeVisible,
                children: [
                  /* @__PURE__ */ jsx11("span", { children: isCodeVisible ? "Hide Code" : "Show Code" }),
                  /* @__PURE__ */ jsx11(
                    ChevronDown2,
                    {
                      className: `w-4 h-4 transition-transform duration-200 ${isCodeVisible ? "rotate-180" : ""}`
                    }
                  )
                ]
              }
            ),
            isCodeVisible && /* @__PURE__ */ jsxs6("div", { className: "relative border-t border-stone-700/60 bg-stone-950/60 px-4 py-4 rounded-b-lg", children: [
              /* @__PURE__ */ jsx11(
                "button",
                {
                  type: "button",
                  onClick: handleCodeCopy,
                  className: "absolute top-3 right-3 flex items-center gap-1 rounded-md border border-stone-700 bg-stone-800 px-2 py-1 text-xs font-medium text-white shadow hover:bg-stone-700",
                  children: codeCopied ? /* @__PURE__ */ jsxs6(Fragment, { children: [
                    /* @__PURE__ */ jsx11(Check2, { className: "h-3.5 w-3.5" }),
                    "Copied"
                  ] }) : /* @__PURE__ */ jsxs6(Fragment, { children: [
                    /* @__PURE__ */ jsx11(Copy, { className: "h-3.5 w-3.5" }),
                    "Copy"
                  ] })
                }
              ),
              /* @__PURE__ */ jsx11("pre", { className: "whitespace-pre overflow-x-auto text-xs md:text-sm text-stone-200 pr-14", children: /* @__PURE__ */ jsx11("code", { className: "block text-stone-200", children: highlightedCode ?? formattedCode }) })
            ] })
          ] }),
          shouldShowCopyButton && /* @__PURE__ */ jsx11("div", { className: "flex-1 pt-4", children: /* @__PURE__ */ jsx11(
            "button",
            {
              onClick: () => {
                const copyPayload = formattedCode || baseSnippet;
                if (!copyPayload) return;
                navigator.clipboard.writeText(copyPayload);
                setCopied(true);
                setTimeout(() => setCopied(false), 5e3);
              },
              className: "w-full px-4 py-2 text-sm bg-stone-800 hover:bg-stone-700 text-white rounded-md flex items-center justify-center gap-2 shadow",
              children: copied ? /* @__PURE__ */ jsxs6(Fragment, { children: [
                /* @__PURE__ */ jsx11(Check2, { className: "w-4 h-4" }),
                "Copied"
              ] }) : /* @__PURE__ */ jsxs6(Fragment, { children: [
                /* @__PURE__ */ jsx11(Copy, { className: "w-4 h-4" }),
                "Copy to Clipboard"
              ] })
            }
          ) }, "control-panel-jsx")
        ] }),
        previewUrl && /* @__PURE__ */ jsxs6("div", { className: "flex flex-col gap-2", children: [
          /* @__PURE__ */ jsx11(Button, { asChild: true, className: "w-full", children: /* @__PURE__ */ jsxs6(
            "a",
            {
              href: previewUrl,
              target: "_blank",
              rel: "noopener noreferrer",
              className: "w-full px-4 py-2 text-sm text-center bg-stone-900 hover:bg-stone-800 text-white rounded-md border border-stone-700",
              children: [
                /* @__PURE__ */ jsx11(SquareArrowOutUpRight, {}),
                " Open in a New Tab"
              ]
            }
          ) }),
          config?.showPresentationButton && presentationUrl && /* @__PURE__ */ jsxs6(
            Button,
            {
              type: "button",
              onClick: handlePresentationClick,
              variant: "secondary",
              className: "w-full bg-stone-800 text-white hover:bg-stone-700 border border-stone-700",
              children: [
                /* @__PURE__ */ jsx11(Presentation, {}),
                " Presentation Mode"
              ]
            }
          )
        ] })
      ] })
    }
  );
};
var ControlPanel_default = ControlPanel;

// src/components/PreviewContainer.tsx
import { useRef as useRef6 } from "react";

// src/components/Grid.tsx
import { jsx as jsx12 } from "react/jsx-runtime";
function Grid() {
  return /* @__PURE__ */ jsx12(
    "div",
    {
      className: "absolute inset-0 w-full h-full z-[0] blur-[1px]",
      style: {
        backgroundImage: `
      linear-gradient(to right,rgb(13, 13, 13) 1px, transparent 1px),
      linear-gradient(to bottom,rgb(13, 13, 13) 1px, transparent 1px)
    `,
        backgroundSize: "1rem 1rem",
        backgroundPosition: "center"
      }
    }
  );
}
var Grid_default = Grid;

// src/components/PreviewContainer.tsx
import { jsx as jsx13, jsxs as jsxs7 } from "react/jsx-runtime";
var PreviewContainer = ({ children, hideControls }) => {
  const { config } = useControlsContext();
  const { leftPanelWidth, isDesktop, isHydrated, containerRef } = useResizableLayout();
  const previewRef = useRef6(null);
  return /* @__PURE__ */ jsx13(
    "div",
    {
      ref: previewRef,
      className: "order-1 md:order-2 flex-1 md:flex-none bg-black overflow-auto flex items-center justify-center relative",
      style: isHydrated && isDesktop && !hideControls ? {
        width: `${100 - leftPanelWidth}%`,
        marginLeft: `${leftPanelWidth}%`
      } : {},
      children: /* @__PURE__ */ jsxs7("div", { className: "w-full h-screen", children: [
        config?.showGrid && /* @__PURE__ */ jsx13(Grid_default, {}),
        /* @__PURE__ */ jsx13("div", { className: "w-full h-full flex items-center justify-center relative", children })
      ] })
    }
  );
};
var PreviewContainer_default = PreviewContainer;

// src/components/Playground.tsx
import { jsx as jsx14, jsxs as jsxs8 } from "react/jsx-runtime";
var HiddenPreview = ({ children }) => /* @__PURE__ */ jsx14("div", { "aria-hidden": "true", className: "hidden", children });
function Playground({ children }) {
  const [isHydrated, setIsHydrated] = useState5(false);
  const [copied, setCopied] = useState5(false);
  useEffect7(() => {
    setIsHydrated(true);
  }, []);
  const { showControls, isPresentationMode, isControlsOnly } = useMemo5(() => {
    if (typeof window === "undefined") {
      return {
        showControls: true,
        isPresentationMode: false,
        isControlsOnly: false
      };
    }
    const params = new URLSearchParams(window.location.search);
    const presentation = params.get(PRESENTATION_PARAM) === "true";
    const controlsOnly = params.get(CONTROLS_ONLY_PARAM) === "true";
    const noControlsParam = params.get(NO_CONTROLS_PARAM) === "true";
    const showControlsValue = controlsOnly || !presentation && !noControlsParam;
    return {
      showControls: showControlsValue,
      isPresentationMode: presentation,
      isControlsOnly: controlsOnly
    };
  }, []);
  const shouldShowShareButton = !showControls && !isPresentationMode;
  const layoutHideControls = !showControls || isControlsOnly;
  const handleCopy = () => {
    navigator.clipboard.writeText(window.location.href);
    setCopied(true);
    setTimeout(() => setCopied(false), 2e3);
  };
  if (!isHydrated) return null;
  return /* @__PURE__ */ jsx14(ResizableLayout, { hideControls: layoutHideControls, children: /* @__PURE__ */ jsxs8(ControlsProvider, { children: [
    shouldShowShareButton && /* @__PURE__ */ jsxs8(
      "button",
      {
        onClick: handleCopy,
        className: "absolute top-4 right-4 z-50 flex items-center gap-1 rounded bg-black/70 px-3 py-1 text-white hover:bg-black",
        children: [
          copied ? /* @__PURE__ */ jsx14(Check3, { size: 16 }) : /* @__PURE__ */ jsx14(Copy2, { size: 16 }),
          copied ? "Copied!" : "Share"
        ]
      }
    ),
    isControlsOnly ? /* @__PURE__ */ jsx14(HiddenPreview, { children }) : /* @__PURE__ */ jsx14(PreviewContainer_default, { hideControls: layoutHideControls, children }),
    showControls && /* @__PURE__ */ jsx14(ControlPanel_default, {})
  ] }) });
}

// src/hooks/useAdvancedPaletteControls.ts
import { useCallback as useCallback5, useEffect as useEffect8, useMemo as useMemo6, useRef as useRef7, useState as useState6 } from "react";
var cloneForCallbacks = (palette) => clonePalette(palette);
var useAdvancedPaletteControls = (options = {}) => {
  const resolvedDefaultPalette = useMemo6(
    () => createAdvancedPalette(options.defaultPalette),
    [options.defaultPalette]
  );
  const resolvedFallbackPalette = useMemo6(
    () => options.fallbackPalette ? createAdvancedPalette(options.fallbackPalette) : resolvedDefaultPalette,
    [options.fallbackPalette, resolvedDefaultPalette]
  );
  const [palette, setPaletteState] = useState6(
    () => clonePalette(resolvedDefaultPalette)
  );
  const defaultSignatureRef = useRef7(
    createPaletteSignature(resolvedDefaultPalette)
  );
  useEffect8(() => {
    const nextSignature = createPaletteSignature(resolvedDefaultPalette);
    if (defaultSignatureRef.current === nextSignature) return;
    defaultSignatureRef.current = nextSignature;
    setPaletteState(clonePalette(resolvedDefaultPalette));
  }, [resolvedDefaultPalette]);
  const notifyChange = useCallback5(
    (nextPalette) => {
      options.onChange?.(cloneForCallbacks(nextPalette));
    },
    [options.onChange]
  );
  const setPalette = useCallback5(
    (source) => {
      const nextPalette = createAdvancedPalette(
        source ?? resolvedDefaultPalette
      );
      setPaletteState(clonePalette(nextPalette));
      notifyChange(nextPalette);
    },
    [notifyChange, resolvedDefaultPalette]
  );
  const updatePalette = useCallback5(
    (updater) => {
      setPaletteState((current) => {
        const nextSource = updater(clonePalette(current));
        const nextPalette = createAdvancedPalette(
          nextSource ?? current ?? resolvedDefaultPalette
        );
        notifyChange(nextPalette);
        return clonePalette(nextPalette);
      });
    },
    [notifyChange, resolvedDefaultPalette]
  );
  const resetPalette = useCallback5(() => {
    setPaletteState(clonePalette(resolvedDefaultPalette));
    notifyChange(resolvedDefaultPalette);
  }, [notifyChange, resolvedDefaultPalette]);
  const handleControlPaletteChange = useCallback5(
    (nextPalette) => {
      setPaletteState(clonePalette(nextPalette));
      notifyChange(nextPalette);
    },
    [notifyChange]
  );
  const controlConfig = useMemo6(
    () => ({
      ...options.control ?? {},
      defaultPalette: resolvedDefaultPalette,
      onPaletteChange: handleControlPaletteChange
    }),
    [handleControlPaletteChange, options.control, resolvedDefaultPalette]
  );
  const hexColors = useMemo6(
    () => advancedPaletteToHexColors(palette, {
      sectionOrder: options.sectionOrder,
      fallbackPalette: resolvedFallbackPalette,
      defaultColor: options.defaultColor
    }),
    [
      options.defaultColor,
      options.sectionOrder,
      palette,
      resolvedFallbackPalette
    ]
  );
  const paletteSignature = useMemo6(
    () => createPaletteSignature(palette),
    [palette]
  );
  const paletteGradient = useMemo6(
    () => computePaletteGradient(palette, options.gradientSteps),
    [options.gradientSteps, palette]
  );
  return {
    palette,
    hexColors,
    controlConfig,
    paletteGradient,
    setPalette,
    updatePalette,
    resetPalette,
    paletteSignature
  };
};
var useDefaultAdvancedPaletteControls = () => useAdvancedPaletteControls({ defaultPalette: DEFAULT_ADVANCED_PALETTE });
export {
  Button,
  ControlsProvider,
  DEFAULT_ADVANCED_PALETTE,
  DEFAULT_HEX_PALETTE,
  Playground,
  advancedPaletteToHexColors,
  clonePalette,
  computePaletteGradient,
  createAdvancedPalette,
  createPaletteSignature,
  hexToPaletteValue,
  paletteValueToHex,
  useAdvancedPaletteControls,
  useControls,
  useDefaultAdvancedPaletteControls,
  useUrlSyncedControls
};