summaryrefslogtreecommitdiff
path: root/apps/web/src
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src')
-rw-r--r--apps/web/src/app/metadata.ts4
-rw-r--r--apps/web/src/app/roadmap/page.tsx247
-rw-r--r--apps/web/src/components/editor/media-panel/views/media.tsx52
-rw-r--r--apps/web/src/components/editor/timeline-track.tsx137
-rw-r--r--apps/web/src/components/footer.tsx264
5 files changed, 481 insertions, 223 deletions
diff --git a/apps/web/src/app/metadata.ts b/apps/web/src/app/metadata.ts
index 6db26811..47f0020b 100644
--- a/apps/web/src/app/metadata.ts
+++ b/apps/web/src/app/metadata.ts
@@ -3,8 +3,8 @@ import { Metadata } from "next";
const title = "OpenCut";
const description =
"A simple but powerful video editor that gets the job done. In your browser.";
-const openGraphImageUrl = "https://opencut.app/opengraph-image.jpg";
-const twitterImageUrl = "/opengraph-image.jpg";
+const openGraphImageUrl = "https://opencut.app/open-graph/default.jpg";
+const twitterImageUrl = "/open-graph/default.jpg";
export const baseMetaData: Metadata = {
metadataBase: new URL("https://opencut.app"),
diff --git a/apps/web/src/app/roadmap/page.tsx b/apps/web/src/app/roadmap/page.tsx
new file mode 100644
index 00000000..6b10db47
--- /dev/null
+++ b/apps/web/src/app/roadmap/page.tsx
@@ -0,0 +1,247 @@
+import { Metadata } from "next";
+import { Badge } from "@/components/ui/badge";
+import { GithubIcon } from "@/components/icons";
+import Link from "next/link";
+import { Footer } from "@/components/footer";
+import { Header } from "@/components/header";
+import ReactMarkdown from "react-markdown";
+import { cn } from "@/lib/utils";
+
+const roadmapItems: {
+ title: string;
+ description: string;
+ status: {
+ text: string;
+ type: "complete" | "pending" | "default" | "info";
+ };
+}[] = [
+ {
+ title: "Start",
+ description:
+ "This is where it all started. Repository created, initial project structure, and the vision for a free, open-source video editor. [Check out the first tweet](https://x.com/mazeincoding/status/1936706642512388188) to see where it started.",
+ status: {
+ text: "Completed",
+ type: "complete",
+ },
+ },
+ {
+ title: "Core UI",
+ description:
+ "Built the foundation - main layout, header, sidebar, timeline container, and basic component structure. Not all functionality yet, but the UI framework that everything else builds on.",
+ status: {
+ text: "Completed",
+ type: "complete",
+ },
+ },
+ {
+ title: "Basic Functionality",
+ description:
+ "The heart of any video editor. Timeline zoom in/out, making clips longer/shorter, dragging elements around, selection, playhead scrubbing. **This part has to be fucking perfect** because it's what users interact with 99% of the time.",
+ status: {
+ text: "In Progress",
+ type: "pending",
+ },
+ },
+ {
+ title: "Export/Preview Logic",
+ description:
+ "The foundation that enables everything else. Real-time preview, video rendering, export functionality. Once this works, we can add effects, filters, transitions - basically everything that makes a video editor powerful.",
+ status: {
+ text: "In Progress",
+ type: "pending",
+ },
+ },
+ {
+ title: "Text",
+ description:
+ "After media, text is the next most important thing. Font selection with custom font imports, text stroke, colors. All the text essential text properties.",
+ status: {
+ text: "Not Started",
+ type: "default",
+ },
+ },
+ {
+ title: "Effects",
+ description:
+ "Adding visual effects to both text and media. Blur, brightness, contrast, saturation, filters, and all the creative tools that make videos pop. This is where the magic happens.",
+ status: {
+ text: "Not Started",
+ type: "default",
+ },
+ },
+ {
+ title: "Transitions",
+ description:
+ "Smooth transitions between clips. Fade in/out, slide, zoom, dissolve, and custom transition effects.",
+ status: {
+ text: "Not Started",
+ type: "default",
+ },
+ },
+ {
+ title: "Refine from Here",
+ description:
+ "Once we nail the above, we have a **solid foundation** to build anything. Advanced features, performance optimizations, mobile support, desktop app.",
+ status: {
+ text: "Future",
+ type: "info",
+ },
+ },
+];
+
+export const metadata: Metadata = {
+ title: "Roadmap - OpenCut",
+ description:
+ "See what's coming next for OpenCut - the free, open-source video editor that respects your privacy.",
+ openGraph: {
+ title: "OpenCut Roadmap - What's Coming Next",
+ description:
+ "See what's coming next for OpenCut - the free, open-source video editor that respects your privacy.",
+ type: "website",
+ images: [
+ {
+ url: "/open-graph/roadmap.jpg",
+ width: 1200,
+ height: 630,
+ alt: "OpenCut Roadmap",
+ },
+ ],
+ },
+ twitter: {
+ card: "summary_large_image",
+ title: "OpenCut Roadmap - What's Coming Next",
+ description:
+ "See what's coming next for OpenCut - the free, open-source video editor that respects your privacy.",
+ images: ["/open-graph/roadmap.jpg"],
+ },
+};
+
+export default function RoadmapPage() {
+ return (
+ <div className="min-h-screen bg-background">
+ <Header />
+ <main className="relative">
+ <div className="absolute inset-0 overflow-hidden pointer-events-none">
+ <div className="absolute -top-40 -right-40 w-96 h-96 bg-gradient-to-br from-muted/20 to-transparent rounded-full blur-3xl" />
+ <div className="absolute top-1/2 -left-40 w-80 h-80 bg-gradient-to-tr from-muted/10 to-transparent rounded-full blur-3xl" />
+ </div>
+ <div className="relative container mx-auto px-4 py-16">
+ <div className="max-w-4xl mx-auto">
+ <div className="text-center mb-10">
+ <Link
+ href="https://github.com/OpenCut-app/OpenCut"
+ target="_blank"
+ >
+ <Badge variant="secondary" className="gap-2 mb-6">
+ <GithubIcon className="h-3 w-3" />
+ Open Source
+ </Badge>
+ </Link>
+ <h1 className="text-5xl md:text-6xl font-bold tracking-tight mb-6">
+ Roadmap
+ </h1>
+ <p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto leading-relaxed">
+ What's coming next for OpenCut (last updated: January 15, 2025)
+ </p>
+ </div>
+ <div className="space-y-6">
+ {roadmapItems.map((item, index) => (
+ <div key={index} className="relative">
+ <div className="flex items-start gap-2">
+ <span className="text-lg font-medium text-muted-foreground select-none leading-[1.5]">
+ {index + 1}.
+ </span>
+ <div className="flex-1 pt-[2px]">
+ <div className="flex items-center gap-2 mb-2">
+ <h3 className="font-medium text-lg">{item.title}</h3>
+ <Badge
+ className={cn("shadow-none", {
+ "!bg-green-500 text-white":
+ item.status.type === "complete",
+ "!bg-yellow-500 text-white":
+ item.status.type === "pending",
+ "!bg-blue-500 text-white":
+ item.status.type === "info",
+ "!bg-foreground/10 text-accent-foreground":
+ item.status.type === "default",
+ })}
+ >
+ {item.status.text}
+ </Badge>
+ </div>
+ <div className="text-foreground/70 leading-relaxed">
+ <ReactMarkdown
+ components={{
+ a: ({ className, children, ...props }) => (
+ <a
+ className={cn(
+ "text-primary hover:underline",
+ className
+ )}
+ target="_blank"
+ rel="noopener noreferrer"
+ {...props}
+ >
+ {children}
+ </a>
+ ),
+ strong: ({ children }) => (
+ <strong className="font-semibold text-foreground">
+ {children}
+ </strong>
+ ),
+ }}
+ >
+ {item.description}
+ </ReactMarkdown>
+ </div>
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+
+ <div className="mt-12 pt-8 border-t border-muted/20">
+ <div className="text-center space-y-4">
+ <h3 className="text-xl font-semibold">Want to Help?</h3>
+ <p className="text-muted-foreground max-w-2xl mx-auto">
+ OpenCut is open source and built by the community. Every
+ contribution, no matter how small, helps us build the best
+ free video editor possible.
+ </p>
+ <div className="flex flex-col sm:flex-row gap-4 justify-center mt-6">
+ <Link
+ href="https://github.com/OpenCut-app/OpenCut/blob/main/.github/CONTRIBUTING.md"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <Badge
+ variant="outline"
+ className="text-sm px-4 py-2 hover:bg-muted/50 transition-colors"
+ >
+ <GithubIcon className="h-4 w-4 mr-2" />
+ Start Contributing
+ </Badge>
+ </Link>
+ <Link
+ href="https://github.com/OpenCut-app/OpenCut/issues"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <Badge
+ variant="outline"
+ className="text-sm px-4 py-2 hover:bg-muted/50 transition-colors"
+ >
+ Report Issues
+ </Badge>
+ </Link>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </main>
+ <Footer />
+ </div>
+ );
+}
diff --git a/apps/web/src/components/editor/media-panel/views/media.tsx b/apps/web/src/components/editor/media-panel/views/media.tsx
index 48e54961..5438737f 100644
--- a/apps/web/src/components/editor/media-panel/views/media.tsx
+++ b/apps/web/src/components/editor/media-panel/views/media.tsx
@@ -207,9 +207,8 @@ export function MediaView() {
<DragOverlay isVisible={isDragOver} />
<div className="p-3 pb-2">
- {/* Button to add/upload media */}
+ {/* Search and filter controls */}
<div className="flex gap-2">
- {/* Search and filter controls */}
<Select value={mediaFilter} onValueChange={setMediaFilter}>
<SelectTrigger className="w-[80px] h-full text-xs">
<SelectValue />
@@ -228,29 +227,6 @@ export function MediaView() {
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
-
- {/* Add media button */}
- <Button
- variant="outline"
- size="sm"
- onClick={handleFileSelect}
- disabled={isProcessing}
- className="flex-none bg-transparent min-w-[30px] whitespace-nowrap overflow-hidden px-2 justify-center items-center"
- >
- {isProcessing ? (
- <>
- <Upload className="h-4 w-4 animate-spin" />
- <span className="hidden md:inline ml-2">{progress}%</span>
- </>
- ) : (
- <>
- <Plus className="h-4 w-4" />
- <span className="hidden sm:inline ml-2" aria-label="Add file">
- Add
- </span>
- </>
- )}
- </Button>
</div>
</div>
@@ -265,8 +241,32 @@ export function MediaView() {
No media in project
</p>
<p className="text-xs text-muted-foreground/70 mt-1">
- Drag files here or use the button above
+ Drag files here or use the button below
</p>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleFileSelect}
+ disabled={isProcessing}
+ className="flex-none bg-transparent min-w-[30px] whitespace-nowrap overflow-hidden px-2 justify-center items-center mt-2"
+ >
+ {isProcessing ? (
+ <>
+ <Upload className="h-4 w-4 animate-spin" />
+ <span className="hidden md:inline ml-2">{progress}%</span>
+ </>
+ ) : (
+ <>
+ <Plus className="h-4 w-4" />
+ <span
+ className="hidden sm:inline ml-2"
+ aria-label="Add file"
+ >
+ Add
+ </span>
+ </>
+ )}
+ </Button>
</div>
) : (
<div
diff --git a/apps/web/src/components/editor/timeline-track.tsx b/apps/web/src/components/editor/timeline-track.tsx
index c3a16b9a..033b79a6 100644
--- a/apps/web/src/components/editor/timeline-track.tsx
+++ b/apps/web/src/components/editor/timeline-track.tsx
@@ -33,6 +33,7 @@ export function TimelineTrackContent({
const { mediaItems } = useMediaStore();
const {
tracks,
+ addTrack,
moveElementToTrack,
updateElementStartTime,
addElementToTrack,
@@ -495,6 +496,16 @@ export function TimelineTrackContent({
e.preventDefault();
e.stopPropagation();
+ // Debug logging
+ console.log(
+ JSON.stringify({
+ message: "Drop event started in timeline track",
+ dataTransferTypes: Array.from(e.dataTransfer.types),
+ trackId: track.id,
+ trackType: track.type,
+ })
+ );
+
// Reset all drag states
dragCounterRef.current = 0;
setIsDropping(false);
@@ -727,68 +738,28 @@ export function TimelineTrackContent({
// Handle position-aware track creation for media elements
if (!isCompatible || dropPosition !== "on") {
- const needsNewTrack = !isCompatible || dropPosition !== "on";
-
- if (needsNewTrack) {
- if (isVideoOrImage) {
- // For video/image, check if we need a main track or additional media track
- const mainTrack = getMainTrack(tracks);
-
- if (!mainTrack) {
- // No main track exists, create it
- const updatedTracks = ensureMainTrack(tracks);
- const newMainTrack = getMainTrack(updatedTracks);
- if (newMainTrack && newMainTrack.elements.length === 0) {
- targetTrackId = newMainTrack.id;
- targetTrack = newMainTrack;
- } else {
- // Main track was created but somehow has elements, create new media track
- const mainTrackIndex = updatedTracks.findIndex(
- (t) => t.id === newMainTrack?.id
- );
- targetTrackId = insertTrackAt("media", mainTrackIndex);
- const updatedTracksAfterInsert =
- useTimelineStore.getState().tracks;
- const newTargetTrack = updatedTracksAfterInsert.find(
- (t) => t.id === targetTrackId
- );
- if (!newTargetTrack) return;
- targetTrack = newTargetTrack;
- }
- } else if (
- mainTrack.elements.length === 0 &&
- dropPosition === "on"
- ) {
- // Main track exists and is empty, use it
- targetTrackId = mainTrack.id;
- targetTrack = mainTrack;
- } else {
- // Create new media track above main track
- const mainTrackIndex = tracks.findIndex(
- (t) => t.id === mainTrack.id
- );
- let insertIndex: number;
+ if (isVideoOrImage) {
+ // For video/image, check if we need a main track or additional media track
+ const mainTrack = getMainTrack(tracks);
- if (dropPosition === "above") {
- insertIndex = currentTrackIndex;
- } else if (dropPosition === "below") {
- insertIndex = currentTrackIndex + 1;
- } else {
- // Insert above main track
- insertIndex = mainTrackIndex;
- }
-
- targetTrackId = insertTrackAt("media", insertIndex);
- const updatedTracks = useTimelineStore.getState().tracks;
- const newTargetTrack = updatedTracks.find(
- (t) => t.id === targetTrackId
- );
- if (!newTargetTrack) return;
- targetTrack = newTargetTrack;
- }
- } else if (isAudio) {
- // Audio tracks go at the bottom
- const mainTrack = getMainTrack(tracks);
+ if (!mainTrack) {
+ // No main track exists, create it
+ targetTrackId = addTrack("media");
+ const updatedTracks = useTimelineStore.getState().tracks;
+ const newTargetTrack = updatedTracks.find(
+ (t) => t.id === targetTrackId
+ );
+ if (!newTargetTrack) return;
+ targetTrack = newTargetTrack;
+ } else if (
+ mainTrack.elements.length === 0 &&
+ dropPosition === "on"
+ ) {
+ // Main track exists and is empty, use it
+ targetTrackId = mainTrack.id;
+ targetTrack = mainTrack;
+ } else {
+ // Create new media track
let insertIndex: number;
if (dropPosition === "above") {
@@ -796,18 +767,14 @@ export function TimelineTrackContent({
} else if (dropPosition === "below") {
insertIndex = currentTrackIndex + 1;
} else {
- // Insert after main track (bottom area)
- if (mainTrack) {
- const mainTrackIndex = tracks.findIndex(
- (t) => t.id === mainTrack.id
- );
- insertIndex = mainTrackIndex + 1;
- } else {
- insertIndex = tracks.length; // Bottom of timeline
- }
+ // Insert above main track
+ const mainTrackIndex = tracks.findIndex(
+ (t) => t.id === mainTrack.id
+ );
+ insertIndex = mainTrackIndex;
}
- targetTrackId = insertTrackAt("audio", insertIndex);
+ targetTrackId = insertTrackAt("media", insertIndex);
const updatedTracks = useTimelineStore.getState().tracks;
const newTargetTrack = updatedTracks.find(
(t) => t.id === targetTrackId
@@ -815,6 +782,34 @@ export function TimelineTrackContent({
if (!newTargetTrack) return;
targetTrack = newTargetTrack;
}
+ } else if (isAudio) {
+ // Audio tracks go at the bottom
+ const mainTrack = getMainTrack(tracks);
+ let insertIndex: number;
+
+ if (dropPosition === "above") {
+ insertIndex = currentTrackIndex;
+ } else if (dropPosition === "below") {
+ insertIndex = currentTrackIndex + 1;
+ } else {
+ // Insert after main track (bottom area)
+ if (mainTrack) {
+ const mainTrackIndex = tracks.findIndex(
+ (t) => t.id === mainTrack.id
+ );
+ insertIndex = mainTrackIndex + 1;
+ } else {
+ insertIndex = tracks.length; // Bottom of timeline
+ }
+ }
+
+ targetTrackId = insertTrackAt("audio", insertIndex);
+ const updatedTracks = useTimelineStore.getState().tracks;
+ const newTargetTrack = updatedTracks.find(
+ (t) => t.id === targetTrackId
+ );
+ if (!newTargetTrack) return;
+ targetTrack = newTargetTrack;
}
}
diff --git a/apps/web/src/components/footer.tsx b/apps/web/src/components/footer.tsx
index 10f0e119..81505d4a 100644
--- a/apps/web/src/components/footer.tsx
+++ b/apps/web/src/components/footer.tsx
@@ -1,124 +1,140 @@
-"use client";
-
-import { motion } from "motion/react";
-import Link from "next/link";
-import { useEffect, useState } from "react";
-import { RiGithubLine, RiTwitterXLine } from "react-icons/ri";
-import { getStars } from "@/lib/fetch-github-stars";
-import Image from "next/image";
-
-export function Footer() {
- const [star, setStar] = useState<string>();
-
- useEffect(() => {
- const fetchStars = async () => {
- try {
- const data = await getStars();
- setStar(data);
- } catch (err) {
- console.error("Failed to fetch GitHub stars", err);
- }
- };
-
- fetchStars();
- }, []);
-
- return (
- <motion.footer
- className="bg-background border-t"
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- transition={{ delay: 0.8, duration: 0.8 }}
- >
- <div className="max-w-5xl mx-auto px-8 py-10">
- <div className="grid grid-cols-1 md:grid-cols-2 gap-12 mb-8">
- {/* Brand Section */}
- <div className="md:col-span-1 max-w-sm">
- <div className="flex items-center gap-2 mb-4">
- <Image src="/logo.svg" alt="OpenCut" width={24} height={24} />
- <span className="font-bold text-lg">OpenCut</span>
- </div>
- <p className="text-sm text-muted-foreground mb-5">
- The open source video editor that gets the job done. Simple,
- powerful, and works on any platform.
- </p>
- <div className="flex gap-3">
- <Link
- href="https://github.com/OpenCut-app/OpenCut"
- className="text-muted-foreground hover:text-foreground transition-colors"
- target="_blank"
- rel="noopener noreferrer"
- >
- <RiGithubLine className="h-5 w-5" />
- </Link>
- <Link
- href="https://x.com/OpenCutApp"
- className="text-muted-foreground hover:text-foreground transition-colors"
- target="_blank"
- rel="noopener noreferrer"
- >
- <RiTwitterXLine className="h-5 w-5" />
- </Link>
- </div>
- </div>
-
- <div className="flex gap-12 justify-end items-start py-2">
- <div>
- <h3 className="font-semibold text-foreground mb-4">Resources</h3>
- <ul className="space-y-2 text-sm">
- <li>
- <Link
- href="/privacy"
- className="text-muted-foreground hover:text-foreground transition-colors"
- >
- Privacy policy
- </Link>
- </li>
- <li>
- <Link
- href="/terms"
- className="text-muted-foreground hover:text-foreground transition-colors"
- >
- Terms of use
- </Link>
- </li>
- </ul>
- </div>
-
- {/* Company Links */}
- <div>
- <h3 className="font-semibold text-foreground mb-4">Company</h3>
- <ul className="space-y-2 text-sm">
- <li>
- <Link
- href="/contributors"
- className="text-muted-foreground hover:text-foreground transition-colors"
- >
- Contributors
- </Link>
- </li>
- <li>
- <Link
- href="https://github.com/OpenCut-app/OpenCut/blob/main/README.md"
- className="text-muted-foreground hover:text-foreground transition-colors"
- target="_blank"
- rel="noopener noreferrer"
- >
- About
- </Link>
- </li>
- </ul>
- </div>
- </div>
- </div>
-
- {/* Bottom Section */}
- <div className="pt-2 flex flex-col md:flex-row justify-between items-center gap-4">
- <div className="flex items-center gap-4 text-sm text-muted-foreground">
- <span>© 2025 OpenCut, All Rights Reserved</span>
- </div>
- </div>
- </div>
- </motion.footer>
- );
-}
+"use client";
+
+import { motion } from "motion/react";
+import Link from "next/link";
+import { useEffect, useState } from "react";
+import { RiDiscordFill, RiGithubLine, RiTwitterXLine } from "react-icons/ri";
+import { getStars } from "@/lib/fetch-github-stars";
+import Image from "next/image";
+
+export function Footer() {
+ const [star, setStar] = useState<string>();
+
+ useEffect(() => {
+ const fetchStars = async () => {
+ try {
+ const data = await getStars();
+ setStar(data);
+ } catch (err) {
+ console.error("Failed to fetch GitHub stars", err);
+ }
+ };
+
+ fetchStars();
+ }, []);
+
+ return (
+ <motion.footer
+ className="bg-background border-t"
+ initial={{ opacity: 0 }}
+ animate={{ opacity: 1 }}
+ transition={{ delay: 0.8, duration: 0.8 }}
+ >
+ <div className="max-w-5xl mx-auto px-8 py-10">
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-12 mb-8">
+ {/* Brand Section */}
+ <div className="md:col-span-1 max-w-sm">
+ <div className="flex items-center gap-2 mb-4">
+ <Image src="/logo.svg" alt="OpenCut" width={24} height={24} />
+ <span className="font-bold text-lg">OpenCut</span>
+ </div>
+ <p className="text-sm text-muted-foreground mb-5">
+ The open source video editor that gets the job done. Simple,
+ powerful, and works on any platform.
+ </p>
+ <div className="flex gap-3">
+ <Link
+ href="https://github.com/OpenCut-app/OpenCut"
+ className="text-muted-foreground hover:text-foreground transition-colors"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <RiGithubLine className="h-5 w-5" />
+ </Link>
+ <Link
+ href="https://x.com/OpenCutApp"
+ className="text-muted-foreground hover:text-foreground transition-colors"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <RiTwitterXLine className="h-5 w-5" />
+ </Link>
+ <Link
+ href="https://discord.com/invite/Mu3acKZvCp"
+ className="text-muted-foreground hover:text-foreground transition-colors"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <RiDiscordFill className="h-5 w-5" />
+ </Link>
+ </div>
+ </div>
+
+ <div className="flex gap-12 justify-end items-start py-2">
+ <div>
+ <h3 className="font-semibold text-foreground mb-4">Resources</h3>
+ <ul className="space-y-2 text-sm">
+ <li>
+ <Link
+ href="/roadmap"
+ className="text-muted-foreground hover:text-foreground transition-colors"
+ >
+ Roadmap
+ </Link>
+ </li>
+ <li>
+ <Link
+ href="/privacy"
+ className="text-muted-foreground hover:text-foreground transition-colors"
+ >
+ Privacy policy
+ </Link>
+ </li>
+ <li>
+ <Link
+ href="/terms"
+ className="text-muted-foreground hover:text-foreground transition-colors"
+ >
+ Terms of use
+ </Link>
+ </li>
+ </ul>
+ </div>
+
+ {/* Company Links */}
+ <div>
+ <h3 className="font-semibold text-foreground mb-4">Company</h3>
+ <ul className="space-y-2 text-sm">
+ <li>
+ <Link
+ href="/contributors"
+ className="text-muted-foreground hover:text-foreground transition-colors"
+ >
+ Contributors
+ </Link>
+ </li>
+ <li>
+ <Link
+ href="https://github.com/OpenCut-app/OpenCut/blob/main/README.md"
+ className="text-muted-foreground hover:text-foreground transition-colors"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ About
+ </Link>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+
+ {/* Bottom Section */}
+ <div className="pt-2 flex flex-col md:flex-row justify-between items-center gap-4">
+ <div className="flex items-center gap-4 text-sm text-muted-foreground">
+ <span>© 2025 OpenCut, All Rights Reserved</span>
+ </div>
+ </div>
+ </div>
+ </motion.footer>
+ );
+}