feat: scenes button to timeline toolbar

This commit is contained in:
Maze Winther
2025-08-31 23:25:26 +02:00
parent e43d79e4c5
commit 2b700b7474
2 changed files with 509 additions and 411 deletions
@@ -1,411 +1,419 @@
import { usePlaybackStore } from "@/stores/playback-store"; import { usePlaybackStore } from "@/stores/playback-store";
import { useProjectStore } from "@/stores/project-store"; import { useProjectStore } from "@/stores/project-store";
import { useTimelineStore } from "@/stores/timeline-store"; import { useTimelineStore } from "@/stores/timeline-store";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
TooltipProvider, TooltipProvider,
Tooltip, Tooltip,
TooltipTrigger, TooltipTrigger,
TooltipContent, TooltipContent,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Pause, Pause,
Play, Play,
SkipBack, SkipBack,
Bookmark, Bookmark,
Magnet, Magnet,
Link, Link,
ZoomOut, ZoomOut,
ZoomIn, ZoomIn,
Copy, Copy,
Trash2, Trash2,
Snowflake, Snowflake,
ArrowLeftToLine, ArrowLeftToLine,
ArrowRightToLine, ArrowRightToLine,
SplitSquareHorizontal, SplitSquareHorizontal,
Scissors, Scissors,
} from "lucide-react"; LayersIcon,
import { Slider } from "@/components/ui/slider"; } from "lucide-react";
import { DEFAULT_FPS } from "@/stores/project-store"; import {
import { formatTimeCode } from "@/lib/time"; SplitButton,
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; SplitButtonLeft,
import { EditableTimecode } from "@/components/ui/editable-timecode"; SplitButtonRight,
SplitButtonSeparator,
export function TimelineToolbar({ } from "@/components/ui/split-button";
zoomLevel, import { Slider } from "@/components/ui/slider";
setZoomLevel, import { DEFAULT_FPS } from "@/stores/project-store";
}: { import { formatTimeCode } from "@/lib/time";
zoomLevel: number; import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
setZoomLevel: (zoom: number) => void; import { EditableTimecode } from "@/components/ui/editable-timecode";
}) {
const { export function TimelineToolbar({
tracks, zoomLevel,
addTrack, setZoomLevel,
addElementToTrack, }: {
removeElementFromTrack, zoomLevel: number;
removeElementFromTrackWithRipple, setZoomLevel: (zoom: number) => void;
selectedElements, }) {
clearSelectedElements, const {
splitElement, tracks,
splitAndKeepLeft, addTrack,
splitAndKeepRight, addElementToTrack,
separateAudio, removeElementFromTrack,
snappingEnabled, removeElementFromTrackWithRipple,
toggleSnapping, selectedElements,
rippleEditingEnabled, clearSelectedElements,
toggleRippleEditing, splitElement,
} = useTimelineStore(); splitAndKeepLeft,
const { currentTime, duration, isPlaying, toggle, seek } = usePlaybackStore(); splitAndKeepRight,
const { toggleBookmark, isBookmarked, activeProject } = useProjectStore(); separateAudio,
snappingEnabled,
const handleSplitSelected = () => { toggleSnapping,
if (selectedElements.length === 0) return; rippleEditingEnabled,
let splitCount = 0; toggleRippleEditing,
selectedElements.forEach(({ trackId, elementId }) => { } = useTimelineStore();
const track = tracks.find((t) => t.id === trackId); const { currentTime, duration, isPlaying, toggle, seek } = usePlaybackStore();
const element = track?.elements.find((c) => c.id === elementId); const { toggleBookmark, isBookmarked, activeProject } = useProjectStore();
if (element && track) {
const effectiveStart = element.startTime; const handleSplitSelected = () => {
const effectiveEnd = if (selectedElements.length === 0) return;
element.startTime + let splitCount = 0;
(element.duration - element.trimStart - element.trimEnd); selectedElements.forEach(({ trackId, elementId }) => {
if (currentTime > effectiveStart && currentTime < effectiveEnd) { const track = tracks.find((t) => t.id === trackId);
const newElementId = splitElement(trackId, elementId, currentTime); const element = track?.elements.find((c) => c.id === elementId);
if (newElementId) splitCount++; if (element && track) {
} const effectiveStart = element.startTime;
} const effectiveEnd =
}); element.startTime +
if (splitCount === 0) { (element.duration - element.trimStart - element.trimEnd);
toast.error("Playhead must be within selected elements to split"); if (currentTime > effectiveStart && currentTime < effectiveEnd) {
} const newElementId = splitElement(trackId, elementId, currentTime);
}; if (newElementId) splitCount++;
}
const handleDuplicateSelected = () => { }
if (selectedElements.length === 0) return; });
const canDuplicate = selectedElements.length === 1; if (splitCount === 0) {
if (!canDuplicate) return; toast.error("Playhead must be within selected elements to split");
}
selectedElements.forEach(({ trackId, elementId }) => { };
const track = tracks.find((t) => t.id === trackId);
const element = track?.elements.find((el) => el.id === elementId); const handleDuplicateSelected = () => {
if (element) { if (selectedElements.length === 0) return;
const newStartTime = const canDuplicate = selectedElements.length === 1;
element.startTime + if (!canDuplicate) return;
(element.duration - element.trimStart - element.trimEnd) +
0.1; selectedElements.forEach(({ trackId, elementId }) => {
const { id, ...elementWithoutId } = element; const track = tracks.find((t) => t.id === trackId);
addElementToTrack(trackId, { const element = track?.elements.find((el) => el.id === elementId);
...elementWithoutId, if (element) {
startTime: newStartTime, const newStartTime =
}); element.startTime +
} (element.duration - element.trimStart - element.trimEnd) +
}); 0.1;
clearSelectedElements(); const { id, ...elementWithoutId } = element;
}; addElementToTrack(trackId, {
...elementWithoutId,
const handleFreezeSelected = () => { startTime: newStartTime,
toast.info("Freeze frame functionality coming soon!"); });
}; }
});
const handleSplitAndKeepLeft = () => { clearSelectedElements();
if (selectedElements.length !== 1) { };
toast.error("Select exactly one element");
return; const handleFreezeSelected = () => {
} toast.info("Freeze frame functionality coming soon!");
const { trackId, elementId } = selectedElements[0]; };
const track = tracks.find((t) => t.id === trackId);
const element = track?.elements.find((c) => c.id === elementId); const handleSplitAndKeepLeft = () => {
if (!element) return; if (selectedElements.length !== 1) {
const effectiveStart = element.startTime; toast.error("Select exactly one element");
const effectiveEnd = return;
element.startTime + }
(element.duration - element.trimStart - element.trimEnd); const { trackId, elementId } = selectedElements[0];
if (currentTime <= effectiveStart || currentTime >= effectiveEnd) { const track = tracks.find((t) => t.id === trackId);
toast.error("Playhead must be within selected element"); const element = track?.elements.find((c) => c.id === elementId);
return; if (!element) return;
} const effectiveStart = element.startTime;
splitAndKeepLeft(trackId, elementId, currentTime); const effectiveEnd =
}; element.startTime +
(element.duration - element.trimStart - element.trimEnd);
const handleSplitAndKeepRight = () => { if (currentTime <= effectiveStart || currentTime >= effectiveEnd) {
if (selectedElements.length !== 1) { toast.error("Playhead must be within selected element");
toast.error("Select exactly one element"); return;
return; }
} splitAndKeepLeft(trackId, elementId, currentTime);
const { trackId, elementId } = selectedElements[0]; };
const track = tracks.find((t) => t.id === trackId);
const element = track?.elements.find((c) => c.id === elementId); const handleSplitAndKeepRight = () => {
if (!element) return; if (selectedElements.length !== 1) {
const effectiveStart = element.startTime; toast.error("Select exactly one element");
const effectiveEnd = return;
element.startTime + }
(element.duration - element.trimStart - element.trimEnd); const { trackId, elementId } = selectedElements[0];
if (currentTime <= effectiveStart || currentTime >= effectiveEnd) { const track = tracks.find((t) => t.id === trackId);
toast.error("Playhead must be within selected element"); const element = track?.elements.find((c) => c.id === elementId);
return; if (!element) return;
} const effectiveStart = element.startTime;
splitAndKeepRight(trackId, elementId, currentTime); const effectiveEnd =
}; element.startTime +
(element.duration - element.trimStart - element.trimEnd);
const handleSeparateAudio = () => { if (currentTime <= effectiveStart || currentTime >= effectiveEnd) {
if (selectedElements.length !== 1) { toast.error("Playhead must be within selected element");
toast.error("Select exactly one media element to separate audio"); return;
return; }
} splitAndKeepRight(trackId, elementId, currentTime);
const { trackId, elementId } = selectedElements[0]; };
const track = tracks.find((t) => t.id === trackId);
if (!track || track.type !== "media") { const handleSeparateAudio = () => {
toast.error("Select a media element to separate audio"); if (selectedElements.length !== 1) {
return; toast.error("Select exactly one media element to separate audio");
} return;
separateAudio(trackId, elementId); }
}; const { trackId, elementId } = selectedElements[0];
const track = tracks.find((t) => t.id === trackId);
const handleDeleteSelected = () => { if (!track || track.type !== "media") {
if (selectedElements.length === 0) return; toast.error("Select a media element to separate audio");
selectedElements.forEach(({ trackId, elementId }) => { return;
if (rippleEditingEnabled) { }
removeElementFromTrackWithRipple(trackId, elementId); separateAudio(trackId, elementId);
} else { };
removeElementFromTrack(trackId, elementId);
} const handleDeleteSelected = () => {
}); if (selectedElements.length === 0) return;
clearSelectedElements(); selectedElements.forEach(({ trackId, elementId }) => {
}; if (rippleEditingEnabled) {
removeElementFromTrackWithRipple(trackId, elementId);
const handleZoomIn = () => { } else {
setZoomLevel(Math.min(4, zoomLevel + 0.25)); removeElementFromTrack(trackId, elementId);
}; }
});
const handleZoomOut = () => { clearSelectedElements();
setZoomLevel(Math.max(0.25, zoomLevel - 0.25)); };
};
const handleZoomIn = () => {
const handleZoomSliderChange = (values: number[]) => { setZoomLevel(Math.min(4, zoomLevel + 0.25));
setZoomLevel(values[0]); };
};
const handleZoomOut = () => {
const handleToggleBookmark = async () => { setZoomLevel(Math.max(0.25, zoomLevel - 0.25));
await toggleBookmark(currentTime); };
};
const handleZoomSliderChange = (values: number[]) => {
const currentBookmarked = isBookmarked(currentTime); setZoomLevel(values[0]);
return ( };
<div className=" flex items-center justify-between px-2 py-1 border-b">
<div className="flex items-center gap-1 w-full"> const handleToggleBookmark = async () => {
<TooltipProvider delayDuration={500}> await toggleBookmark(currentTime);
<Tooltip> };
<TooltipTrigger asChild>
<Button const currentBookmarked = isBookmarked(currentTime);
variant="text" return (
size="icon" <div className="flex items-center justify-between px-2 py-1 border-b h-10">
onClick={toggle} <div className="flex items-center gap-1">
> <TooltipProvider delayDuration={500}>
{isPlaying ? ( <Tooltip>
<Pause className="h-4 w-4" /> <TooltipTrigger asChild>
) : ( <Button variant="text" size="icon" onClick={toggle}>
<Play className="h-4 w-4" /> {isPlaying ? (
)} <Pause className="h-4 w-4" />
</Button> ) : (
</TooltipTrigger> <Play className="h-4 w-4" />
<TooltipContent> )}
{isPlaying ? "Pause (Space)" : "Play (Space)"} </Button>
</TooltipContent> </TooltipTrigger>
</Tooltip> <TooltipContent>
<Tooltip> {isPlaying ? "Pause (Space)" : "Play (Space)"}
<TooltipTrigger asChild> </TooltipContent>
<Button </Tooltip>
variant="text" <Tooltip>
size="icon" <TooltipTrigger asChild>
onClick={() => seek(0)} <Button variant="text" size="icon" onClick={() => seek(0)}>
> <SkipBack className="h-4 w-4" />
<SkipBack className="h-4 w-4" /> </Button>
</Button> </TooltipTrigger>
</TooltipTrigger> <TooltipContent>Return to Start (Home / Enter)</TooltipContent>
<TooltipContent>Return to Start (Home / Enter)</TooltipContent> </Tooltip>
</Tooltip> <div className="w-px h-6 bg-border mx-1" />
<div className="w-px h-6 bg-border mx-1" /> {/* Time Display */}
{/* Time Display */} <div className="flex flex-row items-center justify-center px-2">
<div className="flex flex-row items-center justify-center px-2"> <EditableTimecode
<EditableTimecode time={currentTime}
time={currentTime} duration={duration}
duration={duration} format="HH:MM:SS:FF"
format="HH:MM:SS:FF" fps={activeProject?.fps ?? DEFAULT_FPS}
fps={activeProject?.fps ?? DEFAULT_FPS} onTimeChange={seek}
onTimeChange={seek} className="text-center"
className="text-center" />
/> <div className="text-xs text-muted-foreground font-mono px-2">
<div className="text-xs text-muted-foreground font-mono px-2"> /
/ </div>
</div> <div className="text-xs text-muted-foreground font-mono text-center">
<div className="text-xs text-muted-foreground font-mono text-center"> {formatTimeCode(duration, "HH:MM:SS:FF")}
{formatTimeCode(duration, "HH:MM:SS:FF")} </div>
</div> </div>
</div> {tracks.length === 0 && (
{tracks.length === 0 && ( <>
<> <div className="w-px h-6 bg-border mx-1" />
<div className="w-px h-6 bg-border mx-1" /> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <Button
<Button variant="outline"
variant="outline" size="sm"
size="sm" onClick={() => {
onClick={() => { const trackId = addTrack("media");
const trackId = addTrack("media"); addElementToTrack(trackId, {
addElementToTrack(trackId, { type: "media",
type: "media", mediaId: "test",
mediaId: "test", name: "Test Clip",
name: "Test Clip", duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION, startTime: 0,
startTime: 0, trimStart: 0,
trimStart: 0, trimEnd: 0,
trimEnd: 0, });
}); }}
}} className="text-xs"
className="text-xs" >
> Add Test Clip
Add Test Clip </Button>
</Button> </TooltipTrigger>
</TooltipTrigger> <TooltipContent>Add a test clip to try playback</TooltipContent>
<TooltipContent>Add a test clip to try playback</TooltipContent> </Tooltip>
</Tooltip> </>
</> )}
)} <div className="w-px h-6 bg-border mx-1" />
<div className="w-px h-6 bg-border mx-1" /> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <Button variant="text" size="icon" onClick={handleSplitSelected}>
<Button variant="text" size="icon" onClick={handleSplitSelected}> <Scissors className="h-4 w-4" />
<Scissors className="h-4 w-4" /> </Button>
</Button> </TooltipTrigger>
</TooltipTrigger> <TooltipContent>Split element (Ctrl+S)</TooltipContent>
<TooltipContent>Split element (Ctrl+S)</TooltipContent> </Tooltip>
</Tooltip> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <Button
<Button variant="text"
variant="text" size="icon"
size="icon" onClick={handleSplitAndKeepLeft}
onClick={handleSplitAndKeepLeft} >
> <ArrowLeftToLine className="h-4 w-4" />
<ArrowLeftToLine className="h-4 w-4" /> </Button>
</Button> </TooltipTrigger>
</TooltipTrigger> <TooltipContent>Split and keep left (Ctrl+Q)</TooltipContent>
<TooltipContent>Split and keep left (Ctrl+Q)</TooltipContent> </Tooltip>
</Tooltip> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <Button
<Button variant="text"
variant="text" size="icon"
size="icon" onClick={handleSplitAndKeepRight}
onClick={handleSplitAndKeepRight} >
> <ArrowRightToLine className="h-4 w-4" />
<ArrowRightToLine className="h-4 w-4" /> </Button>
</Button> </TooltipTrigger>
</TooltipTrigger> <TooltipContent>Split and keep right (Ctrl+W)</TooltipContent>
<TooltipContent>Split and keep right (Ctrl+W)</TooltipContent> </Tooltip>
</Tooltip> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <Button variant="text" size="icon" onClick={handleSeparateAudio}>
<Button variant="text" size="icon" onClick={handleSeparateAudio}> <SplitSquareHorizontal className="h-4 w-4" />
<SplitSquareHorizontal className="h-4 w-4" /> </Button>
</Button> </TooltipTrigger>
</TooltipTrigger> <TooltipContent>Separate audio (Ctrl+D)</TooltipContent>
<TooltipContent>Separate audio (Ctrl+D)</TooltipContent> </Tooltip>
</Tooltip> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <Button
<Button variant="text"
variant="text" size="icon"
size="icon" onClick={handleDuplicateSelected}
onClick={handleDuplicateSelected} >
> <Copy className="h-4 w-4" />
<Copy className="h-4 w-4" /> </Button>
</Button> </TooltipTrigger>
</TooltipTrigger> <TooltipContent>Duplicate element (Ctrl+D)</TooltipContent>
<TooltipContent>Duplicate element (Ctrl+D)</TooltipContent> </Tooltip>
</Tooltip> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <Button variant="text" size="icon" onClick={handleFreezeSelected}>
<Button variant="text" size="icon" onClick={handleFreezeSelected}> <Snowflake className="h-4 w-4" />
<Snowflake className="h-4 w-4" /> </Button>
</Button> </TooltipTrigger>
</TooltipTrigger> <TooltipContent>Freeze frame (F)</TooltipContent>
<TooltipContent>Freeze frame (F)</TooltipContent> </Tooltip>
</Tooltip> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <Button variant="text" size="icon" onClick={handleDeleteSelected}>
<Button variant="text" size="icon" onClick={handleDeleteSelected}> <Trash2 className="h-4 w-4" />
<Trash2 className="h-4 w-4" /> </Button>
</Button> </TooltipTrigger>
</TooltipTrigger> <TooltipContent>Delete element (Delete)</TooltipContent>
<TooltipContent>Delete element (Delete)</TooltipContent> </Tooltip>
</Tooltip> <div className="w-px h-6 bg-border mx-1" />
<div className="w-px h-6 bg-border mx-1" /> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <Button variant="text" size="icon" onClick={handleToggleBookmark}>
<Button variant="text" size="icon" onClick={handleToggleBookmark}> <Bookmark
<Bookmark className={`h-4 w-4 ${currentBookmarked ? "fill-primary text-primary" : ""}`}
className={`h-4 w-4 ${currentBookmarked ? "fill-primary text-primary" : ""}`} />
/> </Button>
</Button> </TooltipTrigger>
</TooltipTrigger> <TooltipContent>
<TooltipContent> {currentBookmarked ? "Remove bookmark" : "Add bookmark"}
{currentBookmarked ? "Remove bookmark" : "Add bookmark"} </TooltipContent>
</TooltipContent> </Tooltip>
</Tooltip> </TooltipProvider>
</TooltipProvider> </div>
</div> <div>
<div className="flex items-center gap-1"> <SplitButton>
<TooltipProvider delayDuration={500}> <SplitButtonLeft>Main scene</SplitButtonLeft>
<Tooltip> <SplitButtonSeparator />
<TooltipTrigger asChild> <SplitButtonRight onClick={() => {}}>
<Button variant="text" size="icon" onClick={toggleSnapping}> <LayersIcon />
{snappingEnabled ? ( </SplitButtonRight>
<Magnet className="h-4 w-4 text-primary" /> </SplitButton>
) : ( </div>
<Magnet className="h-4 w-4" /> <div className="flex items-center gap-1">
)} <TooltipProvider delayDuration={500}>
</Button> <Tooltip>
</TooltipTrigger> <TooltipTrigger asChild>
<TooltipContent>Auto snapping</TooltipContent> <Button variant="text" size="icon" onClick={toggleSnapping}>
</Tooltip> {snappingEnabled ? (
<Tooltip> <Magnet className="h-4 w-4 text-primary" />
<TooltipTrigger asChild> ) : (
<Button variant="text" size="icon" onClick={toggleRippleEditing}> <Magnet className="h-4 w-4" />
<Link )}
className={`h-4 w-4 ${ </Button>
rippleEditingEnabled ? "text-primary" : "" </TooltipTrigger>
}`} <TooltipContent>Auto snapping</TooltipContent>
/> </Tooltip>
</Button> <Tooltip>
</TooltipTrigger> <TooltipTrigger asChild>
<TooltipContent> <Button variant="text" size="icon" onClick={toggleRippleEditing}>
{rippleEditingEnabled <Link
? "Disable Ripple Editing" className={`h-4 w-4 ${
: "Enable Ripple Editing"} rippleEditingEnabled ? "text-primary" : ""
</TooltipContent> }`}
</Tooltip> />
</TooltipProvider> </Button>
</TooltipTrigger>
<div className="h-6 w-px bg-border mx-1" /> <TooltipContent>
<div className="flex items-center gap-1"> {rippleEditingEnabled
<Button variant="text" size="icon" onClick={handleZoomOut}> ? "Disable Ripple Editing"
<ZoomOut className="h-4 w-4" /> : "Enable Ripple Editing"}
</Button> </TooltipContent>
<Slider </Tooltip>
className="w-24" </TooltipProvider>
value={[zoomLevel]}
onValueChange={handleZoomSliderChange} <div className="h-6 w-px bg-border mx-1" />
min={0.25} <div className="flex items-center gap-1">
max={4} <Button variant="text" size="icon" onClick={handleZoomOut}>
step={0.25} <ZoomOut className="h-4 w-4" />
/> </Button>
<Button variant="text" size="icon" onClick={handleZoomIn}> <Slider
<ZoomIn className="h-4 w-4" /> className="w-24"
</Button> value={[zoomLevel]}
</div> onValueChange={handleZoomSliderChange}
</div> min={0.25}
</div> max={4}
); step={0.25}
} />
<Button variant="text" size="icon" onClick={handleZoomIn}>
<ZoomIn className="h-4 w-4" />
</Button>
</div>
</div>
</div>
);
}
@@ -0,0 +1,90 @@
import { Button, ButtonProps } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { ReactNode, forwardRef } from "react";
import { cn } from "@/lib/utils";
interface SplitButtonProps {
children: ReactNode;
className?: string;
}
interface SplitButtonSideProps extends Omit<ButtonProps, "variant" | "size"> {
children: ReactNode;
}
const SplitButton = forwardRef<HTMLDivElement, SplitButtonProps>(
({ children, className, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(
"inline-flex rounded-full h-7 border border-input bg-panel-accent overflow-hidden",
className
)}
{...props}
>
{children}
</div>
);
}
);
SplitButton.displayName = "SplitButton";
const SplitButtonSide = forwardRef<
HTMLButtonElement,
SplitButtonSideProps & { paddingClass: string }
>(({ children, className, paddingClass, onClick, ...props }, ref) => {
return (
<Button
ref={ref}
variant="text"
className={cn(
"h-full rounded-none bg-panel-accent !opacity-100 border-0 gap-0 font-normal transition-colors",
onClick
? "hover:bg-foreground/10 hover:opacity-100 cursor-pointer"
: "cursor-default select-text",
paddingClass,
className
)}
onClick={onClick}
{...props}
>
{typeof children === "string" ? (
<span className="font-normal cursor-text">{children}</span>
) : (
children
)}
</Button>
);
});
SplitButtonSide.displayName = "SplitButtonSide";
const SplitButtonLeft = forwardRef<HTMLButtonElement, SplitButtonSideProps>(
({ ...props }, ref) => {
return <SplitButtonSide ref={ref} paddingClass="pl-3 pr-2" {...props} />;
}
);
SplitButtonLeft.displayName = "SplitButtonLeft";
const SplitButtonRight = forwardRef<HTMLButtonElement, SplitButtonSideProps>(
({ ...props }, ref) => {
return <SplitButtonSide ref={ref} paddingClass="pl-2 pr-3" {...props} />;
}
);
SplitButtonRight.displayName = "SplitButtonRight";
const SplitButtonSeparator = forwardRef<HTMLDivElement, { className?: string }>(
({ className, ...props }, ref) => {
return (
<Separator
ref={ref}
orientation="vertical"
className={cn("h-full bg-foreground/15", className)}
{...props}
/>
);
}
);
SplitButtonSeparator.displayName = "SplitButtonSeparator";
export { SplitButton, SplitButtonLeft, SplitButtonRight, SplitButtonSeparator };