diff --git a/package-lock.json b/package-lock.json
index ed487b2..e5d443e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -51,6 +51,7 @@
"date-fns": "^4.1.0",
"embla-carousel-autoplay": "^8.6.0",
"embla-carousel-react": "^8.6.0",
+ "framer-motion": "^12.23.0",
"globals": "^15.14.0",
"input-otp": "^1.4.2",
"konva": "^9.3.20",
diff --git a/package.json b/package.json
index 8a548a2..5578739 100644
--- a/package.json
+++ b/package.json
@@ -70,6 +70,7 @@
"date-fns": "^4.1.0",
"embla-carousel-autoplay": "^8.6.0",
"embla-carousel-react": "^8.6.0",
+ "framer-motion": "^12.23.0",
"globals": "^15.14.0",
"input-otp": "^1.4.2",
"konva": "^9.3.20",
diff --git a/resources/css/app.css b/resources/css/app.css
index 7de2155..87af0e9 100644
--- a/resources/css/app.css
+++ b/resources/css/app.css
@@ -219,31 +219,31 @@ @theme inline {
boxshadow: 0 0 0 8px var(--pulse-color);
}
}
- --animate-gradient: gradient 8s linear infinite
-;
- @keyframes gradient {
- to {
- background-position: var(--bg-size, 300%) 0;
+ --animate-gradient: gradient 8s linear infinite;
+ @keyframes gradient {
+ to {
+ background-position: var(--bg-size, 300%) 0;
}
}
- --animate-marquee: marquee var(--duration) infinite linear;
- --animate-marquee-vertical: marquee-vertical var(--duration) linear infinite;
- @keyframes marquee {
- from {
- transform: translateX(0);
+ --animate-marquee: marquee var(--duration) infinite linear;
+ --animate-marquee-vertical: marquee-vertical var(--duration) linear infinite;
+ @keyframes marquee {
+ from {
+ transform: translateX(0);
}
- to {
- transform: translateX(calc(-100% - var(--gap)));
+ to {
+ transform: translateX(calc(-100% - var(--gap)));
}
}
- @keyframes marquee-vertical {
- from {
- transform: translateY(0);
+ @keyframes marquee-vertical {
+ from {
+ transform: translateY(0);
}
- to {
- transform: translateY(calc(-100% - var(--gap)));
+ to {
+ transform: translateY(calc(-100% - var(--gap)));
}
- }}
+ }
+}
/*
---break---
@@ -256,4 +256,28 @@ @layer base {
body {
@apply bg-background text-foreground;
}
-}
\ No newline at end of file
+}
+
+/* REACTBITS */
+.shiny-text {
+ color: #b5b5b5a4; /* Adjust this color to change intensity/style */
+ background: linear-gradient(120deg, rgba(255, 255, 255, 0) 40%, rgba(255, 255, 255, 0.8) 50%, rgba(255, 255, 255, 0) 60%);
+ background-size: 200% 100%;
+ -webkit-background-clip: text;
+ background-clip: text;
+ display: inline-block;
+ animation: shine 5s linear infinite;
+}
+
+@keyframes shine {
+ 0% {
+ background-position: 100%;
+ }
+ 100% {
+ background-position: -100%;
+ }
+}
+
+.shiny-text.disabled {
+ animation: none;
+}
diff --git a/resources/js/components/reactbits/CountUp/CountUp.jsx b/resources/js/components/reactbits/CountUp/CountUp.jsx
new file mode 100644
index 0000000..8dae984
--- /dev/null
+++ b/resources/js/components/reactbits/CountUp/CountUp.jsx
@@ -0,0 +1,81 @@
+import { useInView, useMotionValue, useSpring } from 'framer-motion';
+import { useEffect, useRef } from 'react';
+
+export default function CountUp({
+ to,
+ from = 0,
+ direction = 'up',
+ delay = 0,
+ duration = 2,
+ className = '',
+ postFix = '',
+ startWhen = true,
+ separator = '',
+ onStart,
+ onEnd,
+}) {
+ const ref = useRef(null);
+ const motionValue = useMotionValue(direction === 'down' ? to : from);
+
+ const damping = 20 + 40 * (1 / duration);
+ const stiffness = 100 * (1 / duration);
+
+ const springValue = useSpring(motionValue, {
+ damping,
+ stiffness,
+ });
+
+ const isInView = useInView(ref, { once: true, margin: '0px' });
+
+ useEffect(() => {
+ if (ref.current) {
+ ref.current.textContent = String(direction === 'down' ? to : from);
+ }
+ }, [from, to, direction]);
+
+ useEffect(() => {
+ if (isInView && startWhen) {
+ if (typeof onStart === 'function') {
+ onStart();
+ }
+
+ const timeoutId = setTimeout(() => {
+ motionValue.set(direction === 'down' ? from : to);
+ }, delay * 1000);
+
+ const durationTimeoutId = setTimeout(
+ () => {
+ if (typeof onEnd === 'function') {
+ onEnd();
+ }
+ },
+ delay * 1000 + duration * 1000,
+ );
+
+ return () => {
+ clearTimeout(timeoutId);
+ clearTimeout(durationTimeoutId);
+ };
+ }
+ }, [isInView, startWhen, motionValue, direction, from, to, delay, onStart, onEnd, duration]);
+
+ useEffect(() => {
+ const unsubscribe = springValue.on('change', (latest) => {
+ if (ref.current) {
+ const options = {
+ useGrouping: !!separator,
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ };
+
+ const formattedNumber = Intl.NumberFormat('en-US', options).format(latest.toFixed(0));
+
+ ref.current.textContent = (separator ? formattedNumber.replace(/,/g, separator) : formattedNumber) + postFix;
+ }
+ });
+
+ return () => unsubscribe();
+ }, [springValue, separator]);
+
+ return ;
+}
diff --git a/resources/js/components/reactbits/ShinyText/ShinyText.jsx b/resources/js/components/reactbits/ShinyText/ShinyText.jsx
new file mode 100644
index 0000000..c6c98b5
--- /dev/null
+++ b/resources/js/components/reactbits/ShinyText/ShinyText.jsx
@@ -0,0 +1,29 @@
+const ShinyText = ({ text, disabled = false, speed = 5, className = '' }) => {
+ const animationDuration = `${speed}s`;
+
+ return (
+
+ {text}
+
+ );
+};
+
+export default ShinyText;
+
+// tailwind.config.js
+// module.exports = {
+// theme: {
+// extend: {
+// keyframes: {
+// shine: {
+// '0%': { 'background-position': '100%' },
+// '100%': { 'background-position': '-100%' },
+// },
+// },
+// animation: {
+// shine: 'shine 5s linear infinite',
+// },
+// },
+// },
+// plugins: [],
+// };
diff --git a/resources/js/components/reactbits/TiltedCard/TiltedCard.jsx b/resources/js/components/reactbits/TiltedCard/TiltedCard.jsx
new file mode 100644
index 0000000..47bd3f4
--- /dev/null
+++ b/resources/js/components/reactbits/TiltedCard/TiltedCard.jsx
@@ -0,0 +1,132 @@
+import { motion, useMotionValue, useSpring } from 'framer-motion';
+import { useRef, useState } from 'react';
+
+const springValues = {
+ damping: 30,
+ stiffness: 100,
+ mass: 2,
+};
+
+export default function TiltedCard({
+ imageSrc,
+ altText = 'Tilted card image',
+ captionText = '',
+ containerHeight = '300px',
+ containerWidth = '100%',
+ imageHeight = '300px',
+ imageWidth = '300px',
+ scaleOnHover = 1.1,
+ rotateAmplitude = 14,
+ showMobileWarning = true,
+ showTooltip = true,
+ overlayContent = null,
+ displayOverlayContent = false,
+}) {
+ const ref = useRef(null);
+ const x = useMotionValue(0);
+ const y = useMotionValue(0);
+ const rotateX = useSpring(useMotionValue(0), springValues);
+ const rotateY = useSpring(useMotionValue(0), springValues);
+ const scale = useSpring(1, springValues);
+ const opacity = useSpring(0);
+ const rotateFigcaption = useSpring(0, {
+ stiffness: 350,
+ damping: 30,
+ mass: 1,
+ });
+
+ const [lastY, setLastY] = useState(0);
+
+ function handleMouse(e) {
+ if (!ref.current) return;
+
+ const rect = ref.current.getBoundingClientRect();
+ const offsetX = e.clientX - rect.left - rect.width / 2;
+ const offsetY = e.clientY - rect.top - rect.height / 2;
+
+ const rotationX = (offsetY / (rect.height / 2)) * -rotateAmplitude;
+ const rotationY = (offsetX / (rect.width / 2)) * rotateAmplitude;
+
+ rotateX.set(rotationX);
+ rotateY.set(rotationY);
+
+ x.set(e.clientX - rect.left);
+ y.set(e.clientY - rect.top);
+
+ const velocityY = offsetY - lastY;
+ rotateFigcaption.set(-velocityY * 0.6);
+ setLastY(offsetY);
+ }
+
+ function handleMouseEnter() {
+ scale.set(scaleOnHover);
+ opacity.set(1);
+ }
+
+ function handleMouseLeave() {
+ opacity.set(0);
+ scale.set(1);
+ rotateX.set(0);
+ rotateY.set(0);
+ rotateFigcaption.set(0);
+ }
+
+ return (
+
+ {showMobileWarning && (
+ This effect is not optimized for mobile. Check on desktop.
+ )}
+
+
+
+
+ {displayOverlayContent && overlayContent && (
+
+ {overlayContent}
+
+ )}
+
+
+ {showTooltip && (
+
+ {captionText}
+
+ )}
+
+ );
+}
diff --git a/resources/js/modules/editor/partials/canvas/video-preview.jsx b/resources/js/modules/editor/partials/canvas/video-preview.jsx
index c0ed958..e56413c 100644
--- a/resources/js/modules/editor/partials/canvas/video-preview.jsx
+++ b/resources/js/modules/editor/partials/canvas/video-preview.jsx
@@ -127,6 +127,16 @@ const VideoPreview = ({
}
};
+ // Pre-select first text node on load
+ useEffect(() => {
+ if (activeElements.length > 0 && !selectedElementId) {
+ const firstTextElement = activeElements.find(element => element.type === 'text');
+ if (firstTextElement) {
+ setSelectedElementId(firstTextElement.id);
+ }
+ }
+ }, [activeElements, selectedElementId, setSelectedElementId]);
+
// Update transformer when selection changes
useEffect(() => {
if (transformerRef.current) {
diff --git a/resources/js/modules/editor/partials/editor-ai-sheet.jsx b/resources/js/modules/editor/partials/editor-ai-sheet.jsx
index 1a403d0..f837f1e 100644
--- a/resources/js/modules/editor/partials/editor-ai-sheet.jsx
+++ b/resources/js/modules/editor/partials/editor-ai-sheet.jsx
@@ -1,24 +1,30 @@
'use client';
-import { Button } from '@/components/ui/button';
-import { Input } from '@/components/ui/input';
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet';
-import { Spinner } from '@/components/ui/spinner';
import { cn } from '@/lib/utils';
import { useMitt } from '@/plugins/MittContext';
-import CoinIcon from '@/reusables/coin-icon';
import useMediaStore from '@/stores/MediaStore';
import useUserStore from '@/stores/UserStore';
import { usePage } from '@inertiajs/react';
-import { useEffect, useState, useRef } from 'react';
+import { useEffect, useRef, useState } from 'react';
import { toast } from 'sonner';
const EditorAISheet = () => {
const [isOpen, setIsOpen] = useState(false);
const [prompt, setPrompt] = useState('');
const emitter = useMitt();
- const { generateMeme, isGeneratingMeme, keywords, isLoadingAIHints, fetchAIHints, checkMemeJobStatus, updateMemeResult, setGeneratingMeme, checkActiveJob } = useMediaStore();
-
+ const {
+ generateMeme,
+ isGeneratingMeme,
+ keywords,
+ isLoadingAIHints,
+ fetchAIHints,
+ checkMemeJobStatus,
+ updateMemeResult,
+ setGeneratingMeme,
+ checkActiveJob,
+ } = useMediaStore();
+
const pollingIntervalRef = useRef(null);
const currentJobIdRef = useRef(null);
@@ -50,7 +56,7 @@ const EditorAISheet = () => {
const response = await checkActiveJob();
if (response?.success?.data) {
const { job_id, status, result } = response.success.data;
-
+
if (status === 'pending' || status === 'processing') {
// Resume polling for active job
setGeneratingMeme(true);
@@ -70,7 +76,7 @@ const EditorAISheet = () => {
const handleOpenChange = (open) => {
setIsOpen(open);
-
+
// If sheet is being closed while generating, stop polling
if (!open && isGeneratingMeme) {
stopPolling();
@@ -79,7 +85,7 @@ const EditorAISheet = () => {
const startPolling = (jobId) => {
currentJobIdRef.current = jobId;
-
+
// Clear existing interval if any
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
@@ -88,10 +94,10 @@ const EditorAISheet = () => {
const checkJobStatus = async () => {
try {
const response = await checkMemeJobStatus(jobId);
-
+
if (response?.success?.data) {
const { status, result, error } = response.success.data;
-
+
if (status === 'completed') {
// Job completed successfully
if (result?.generate) {
@@ -117,7 +123,7 @@ const EditorAISheet = () => {
// Start polling every 5 seconds
pollingIntervalRef.current = setInterval(checkJobStatus, 5000);
-
+
// Also check immediately
checkJobStatus();
};
@@ -151,7 +157,7 @@ const EditorAISheet = () => {
setIsOpen(false);
setPrompt('');
}, 1000);
-
+
return () => clearTimeout(timer);
}
}, [isGeneratingMeme, isOpen]);
@@ -172,6 +178,11 @@ const EditorAISheet = () => {
onEscapeKeyDown={(e) => isGeneratingMeme && e.preventDefault()}
>
+ AI features coming soon!
+
+
+
+ {/*
{isGeneratingMeme ? 'Creating...' : 'What meme would you like to create?'}
@@ -188,7 +199,6 @@ const EditorAISheet = () => {
className="bg-muted/30 max-h-20 min-h-12 resize-none rounded-3xl border-0 p-4 text-center text-base"
/>
- {/* AI Keywords */}
{isLoadingAIHints && Loading AI hints...
}
{keywords.length > 0 && !isLoadingAIHints && (
@@ -259,7 +269,7 @@ const EditorAISheet = () => {
)}
-
+ */}
);
diff --git a/resources/js/modules/editor/partials/editor-header.jsx b/resources/js/modules/editor/partials/editor-header.jsx
index 86dbc40..858b412 100644
--- a/resources/js/modules/editor/partials/editor-header.jsx
+++ b/resources/js/modules/editor/partials/editor-header.jsx
@@ -16,7 +16,7 @@ const EditorHeader = ({ className = '', onNavClick = () => {}, isNavActive = fal
return (
-