"use client"; import { useState } from "react"; import { Popover, PopoverContent } from "@/components/ui/popover"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { cn } from "@/utils/ui"; import { HugeiconsIcon } from "@hugeicons/react"; import { ArrowDown01Icon, Delete02Icon, PlusSignIcon, } from "@hugeicons/core-free-icons"; import { getBezierPoint } from "@/animation/bezier"; import type { NormalizedCubicBezier } from "@/animation/types"; import type { GraphEditorComponentOption } from "./session"; import { BUILTIN_PRESETS, PRESET_MATCH_TOLERANCE, type EasingPreset, } from "./easing-presets"; import { removePreset, savePreset, useCustomPresets } from "./custom-presets-store"; import { BezierGraph, BEZIER_GRAPH_MIN_HEIGHT } from "./bezier-graph"; const COLLAPSED_MAX = 6; const THUMB_SEGMENTS = 24; const THUMB_WIDTH = 40; const THUMB_HEIGHT = 22; const THUMB_PADDING_X = 4; const THUMB_PADDING_Y = 3; const COLLAPSED_GRID_MAX_HEIGHT = 120; const EXPANDED_GRID_MAX_HEIGHT = 240; export function GraphEditorPopover({ children, side, open, onOpenChange, value, message, componentOptions, activeComponentKey, onActiveComponentKeyChange, onPreviewValue, onCommitValue, onCancelPreview, }: { children: React.ReactNode; side?: "top" | "bottom" | "left" | "right"; open?: boolean; onOpenChange?: (open: boolean) => void; value: NormalizedCubicBezier | null; message: string; componentOptions: GraphEditorComponentOption[]; activeComponentKey: string | null; onActiveComponentKeyChange?: (componentKey: string) => void; onPreviewValue?: (value: NormalizedCubicBezier) => void; onCommitValue?: (value: NormalizedCubicBezier) => void; onCancelPreview?: () => void; }) { const [isExpanded, setIsExpanded] = useState(false); const custom = useCustomPresets(); const allPresets = [...BUILTIN_PRESETS, ...custom]; const canEdit = value !== null; const activePresetId = value == null ? null : (allPresets.find((preset) => preset.value.every( (presetValue, index) => Math.abs(presetValue - value[index]) < PRESET_MATCH_TOLERANCE, ), )?.id ?? null); return ( { if (!nextOpen) { onCancelPreview?.(); } onOpenChange?.(nextOpen); }} > {children} {componentOptions.length > 1 && (
{componentOptions.map((component) => ( ))}
)}
{value ? ( ) : ( )}
Presets Saved COLLAPSED_MAX} onExpand={() => setIsExpanded(true)} > {BUILTIN_PRESETS.map((preset) => ( onCommitValue?.(preset.value)} /> ))}
{custom.map((preset) => ( onCommitValue?.(preset.value)} onDelete={() => removePreset({ id: preset.id })} /> ))}
); } function GraphEditorEmptyState({ message }: { message: string }) { return (
{message}
); } function ExpandableGrid({ children, isExpanded, shouldExpand, onExpand, }: { children: React.ReactNode; isExpanded: boolean; shouldExpand: boolean; onExpand: () => void; }) { const gridStyle = shouldExpand ? isExpanded ? { maxHeight: EXPANDED_GRID_MAX_HEIGHT, overflowY: "auto" as const } : { maxHeight: COLLAPSED_GRID_MAX_HEIGHT, overflow: "hidden" as const } : undefined; return (
{children}
{!isExpanded && shouldExpand && (
)}
); } function PresetItem({ preset, isActive, onSelect, onDelete, disabled, }: { preset: EasingPreset; isActive: boolean; onSelect: () => void; onDelete?: () => void; disabled?: boolean; }) { return ( )} ); } function toThumbX({ value }: { value: number }) { return THUMB_PADDING_X + value * (THUMB_WIDTH - THUMB_PADDING_X * 2); } function toThumbY({ value }: { value: number }) { return THUMB_PADDING_Y + (1 - value) * (THUMB_HEIGHT - THUMB_PADDING_Y * 2); } function CurveThumb({ value }: { value: NormalizedCubicBezier }) { const points: string[] = []; for (let i = 0; i <= THUMB_SEGMENTS; i++) { const progress = i / THUMB_SEGMENTS; const x = toThumbX({ value: getBezierPoint({ progress, p0: 0, p1: value[0], p2: value[2], p3: 1 }) }); const y = toThumbY({ value: getBezierPoint({ progress, p0: 0, p1: value[1], p2: value[3], p3: 1 }) }); points.push(`${x},${y}`); } return ( Curve preset preview ); }