From ec3b9b46a1623b87a94df7db0591850e340914fc Mon Sep 17 00:00:00 2001 From: Siddharth Date: Sun, 30 Nov 2025 14:47:22 -0700 Subject: [PATCH 01/16] annotations in preview --- package-lock.json | 331 ++++++++++++++++ package.json | 8 + src/components/ui/popover.tsx | 48 +++ src/components/ui/select.tsx | 160 ++++++++ src/components/ui/toggle-group.tsx | 61 +++ src/components/ui/toggle.tsx | 45 +++ .../video-editor/AnnotationOverlay.tsx | 189 ++++++++++ .../video-editor/AnnotationSettingsPanel.tsx | 357 ++++++++++++++++++ src/components/video-editor/SettingsPanel.tsx | 57 ++- src/components/video-editor/VideoEditor.tsx | 149 +++++++- src/components/video-editor/VideoPlayback.tsx | 34 +- src/components/video-editor/timeline/Item.tsx | 28 +- .../timeline/ItemGlass.module.css | 35 +- .../video-editor/timeline/TimelineEditor.tsx | 122 +++++- src/components/video-editor/types.ts | 67 +++- 15 files changed, 1663 insertions(+), 28 deletions(-) create mode 100644 src/components/ui/popover.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/components/ui/toggle-group.tsx create mode 100644 src/components/ui/toggle.tsx create mode 100644 src/components/video-editor/AnnotationOverlay.tsx create mode 100644 src/components/video-editor/AnnotationSettingsPanel.tsx diff --git a/package-lock.json b/package-lock.json index 1a9f766..8b11eb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,23 +12,31 @@ "@pixi/filter-drop-shadow": "^5.2.0", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-toggle-group": "^1.1.11", "@uiw/color-convert": "^2.9.2", "@uiw/react-color-colorful": "^2.9.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dnd-timeline": "^2.2.0", + "emoji-picker-react": "^4.16.1", + "gsap": "^3.13.0", "lucide-react": "^0.545.0", "mediabunny": "^1.25.1", + "motion": "^12.23.24", "mp4box": "^2.2.0", "pixi.js": "^8.14.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^5.5.0", "react-resizable-panels": "^3.0.6", + "react-rnd": "^10.5.2", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7", @@ -2579,6 +2587,43 @@ } } }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", @@ -2713,6 +2758,49 @@ } } }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slider": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", @@ -2823,6 +2911,60 @@ } } }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", @@ -2959,6 +3101,29 @@ } } }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/rect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", @@ -6291,6 +6456,21 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-picker-react": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.16.1.tgz", + "integrity": "sha512-MrPX0tOCfRL3uYI4of/2GRZ7S6qS7YlacKiF78uFH84/C62vcuHE2DZyv5b4ZJMk0e06es1jjB4e31Bb+YSM8w==", + "license": "MIT", + "dependencies": { + "flairup": "1.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -6905,6 +7085,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/flairup": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/flairup/-/flairup-1.0.0.tgz", + "integrity": "sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==", + "license": "MIT" + }, "node_modules/flat-cache": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", @@ -7005,6 +7191,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.23.24", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz", + "integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -7426,6 +7639,12 @@ "dev": true, "license": "MIT" }, + "node_modules/gsap": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.13.0.tgz", + "integrity": "sha512-QL7MJ2WMjm1PHWsoFrAQH/J8wUeqZvMtHO58qdekHpCfhvhSL4gSiz6vJf5EeMP0LOn3ZCprL2ki/gjED8ghVw==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license." + }, "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -9319,6 +9538,47 @@ "node": ">=10" } }, + "node_modules/motion": { + "version": "12.23.24", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.23.24.tgz", + "integrity": "sha512-Rc5E7oe2YZ72N//S3QXGzbnXgqNrTESv8KKxABR20q2FLch9gHLo0JLyYo2hZ238bZ9Gx6cWhj9VO0IgwbMjCw==", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.23.24", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/mp4box": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/mp4box/-/mp4box-2.2.0.tgz", @@ -10414,6 +10674,17 @@ "node": ">=10" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -10497,6 +10768,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/re-resizable": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.11.2.tgz", + "integrity": "sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -10522,6 +10803,29 @@ "react": "^18.3.1" } }, + "node_modules/react-draggable": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", + "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "license": "MIT", + "dependencies": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-draggable/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/react-icons": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", @@ -10531,6 +10835,12 @@ "react": "*" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -10598,6 +10908,27 @@ "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/react-rnd": { + "version": "10.5.2", + "resolved": "https://registry.npmjs.org/react-rnd/-/react-rnd-10.5.2.tgz", + "integrity": "sha512-0Tm4x7k7pfHf2snewJA8x7Nwgt3LV+58MVEWOVsFjk51eYruFEa6Wy7BNdxt4/lH0wIRsu7Gm3KjSXY2w7YaNw==", + "license": "MIT", + "dependencies": { + "re-resizable": "6.11.2", + "react-draggable": "4.4.6", + "tslib": "2.6.2" + }, + "peerDependencies": { + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, + "node_modules/react-rnd/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD" + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", diff --git a/package.json b/package.json index e15e098..fca930c 100644 --- a/package.json +++ b/package.json @@ -16,23 +16,31 @@ "@pixi/filter-drop-shadow": "^5.2.0", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-toggle-group": "^1.1.11", "@uiw/color-convert": "^2.9.2", "@uiw/react-color-colorful": "^2.9.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dnd-timeline": "^2.2.0", + "emoji-picker-react": "^4.16.1", + "gsap": "^3.13.0", "lucide-react": "^0.545.0", "mediabunny": "^1.25.1", + "motion": "^12.23.24", "mp4box": "^2.2.0", "pixi.js": "^8.14.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^5.5.0", "react-resizable-panels": "^3.0.6", + "react-rnd": "^10.5.2", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7", diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 0000000..01e468b --- /dev/null +++ b/src/components/ui/popover.tsx @@ -0,0 +1,48 @@ +"use client" + +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +function Popover({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverContent({ + className, + align = "center", + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function PopoverAnchor({ + ...props +}: React.ComponentProps) { + return +} + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx new file mode 100644 index 0000000..cbe5a36 --- /dev/null +++ b/src/components/ui/select.tsx @@ -0,0 +1,160 @@ +"use client" + +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { Check, ChevronDown, ChevronUp } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props} + > + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +} diff --git a/src/components/ui/toggle-group.tsx b/src/components/ui/toggle-group.tsx new file mode 100644 index 0000000..1c876bb --- /dev/null +++ b/src/components/ui/toggle-group.tsx @@ -0,0 +1,61 @@ +"use client" + +import * as React from "react" +import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group" +import { type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" +import { toggleVariants } from "@/components/ui/toggle" + +const ToggleGroupContext = React.createContext< + VariantProps +>({ + size: "default", + variant: "default", +}) + +const ToggleGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, size, children, ...props }, ref) => ( + + + {children} + + +)) + +ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName + +const ToggleGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, children, variant, size, ...props }, ref) => { + const context = React.useContext(ToggleGroupContext) + + return ( + + {children} + + ) +}) + +ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName + +export { ToggleGroup, ToggleGroupItem } diff --git a/src/components/ui/toggle.tsx b/src/components/ui/toggle.tsx new file mode 100644 index 0000000..b6f2e7d --- /dev/null +++ b/src/components/ui/toggle.tsx @@ -0,0 +1,45 @@ +"use client" + +import * as React from "react" +import * as TogglePrimitive from "@radix-ui/react-toggle" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const toggleVariants = cva( + "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground", + { + variants: { + variant: { + default: "bg-transparent", + outline: + "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground", + }, + size: { + default: "h-10 px-3", + sm: "h-9 px-2.5", + lg: "h-11 px-5", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +const Toggle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, size, ...props }, ref) => ( + +)) + +Toggle.displayName = TogglePrimitive.Root.displayName + +export { Toggle, toggleVariants } diff --git a/src/components/video-editor/AnnotationOverlay.tsx b/src/components/video-editor/AnnotationOverlay.tsx new file mode 100644 index 0000000..1dd0d9b --- /dev/null +++ b/src/components/video-editor/AnnotationOverlay.tsx @@ -0,0 +1,189 @@ +import { Rnd } from "react-rnd"; +import type { AnnotationRegion } from "./types"; +import { cn } from "@/lib/utils"; + +interface AnnotationOverlayProps { + annotation: AnnotationRegion; + isSelected: boolean; + containerWidth: number; + containerHeight: number; + onPositionChange: (id: string, position: { x: number; y: number }) => void; + onSizeChange: (id: string, size: { width: number; height: number }) => void; + onClick: (id: string) => void; +} + +export function AnnotationOverlay({ + annotation, + isSelected, + containerWidth, + containerHeight, + onPositionChange, + onSizeChange, + onClick, +}: AnnotationOverlayProps) { + const x = (annotation.position.x / 100) * containerWidth; + const y = (annotation.position.y / 100) * containerHeight; + const width = (annotation.size.width / 100) * containerWidth; + const height = (annotation.size.height / 100) * containerHeight; + + console.log('[AnnotationOverlay] Rendering:', { + id: annotation.id, + type: annotation.type, + content: annotation.content.substring(0, 30), + position: annotation.position, + size: annotation.size, + containerWidth, + containerHeight, + calculatedPixels: { x, y, width, height }, + isSelected + }); + + const renderContent = () => { + switch (annotation.type) { + case 'text': + return ( +
+ + {annotation.content} + +
+ ); + + + + case 'image': + if (annotation.content && annotation.content.startsWith('data:image')) { + return ( + Annotation + ); + } + return ( +
+ No image +
+ ); + + default: + return null; + } + }; + + return ( + { + const xPercent = (d.x / containerWidth) * 100; + const yPercent = (d.y / containerHeight) * 100; + onPositionChange(annotation.id, { x: xPercent, y: yPercent }); + }} + onResizeStop={(_e, _direction, ref, _delta, position) => { + const xPercent = (position.x / containerWidth) * 100; + const yPercent = (position.y / containerHeight) * 100; + const widthPercent = (ref.offsetWidth / containerWidth) * 100; + const heightPercent = (ref.offsetHeight / containerHeight) * 100; + onPositionChange(annotation.id, { x: xPercent, y: yPercent }); + onSizeChange(annotation.id, { width: widthPercent, height: heightPercent }); + }} + onClick={() => onClick(annotation.id)} + bounds="parent" + className={cn( + "cursor-move transition-all", + isSelected && "ring-2 ring-[#34B27B] ring-offset-2 ring-offset-transparent" + )} + style={{ + zIndex: 9999, + pointerEvents: isSelected ? 'auto' : 'none', + border: isSelected ? '2px solid rgba(52, 178, 123, 0.8)' : 'none', + backgroundColor: isSelected ? 'rgba(52, 178, 123, 0.1)' : 'transparent', + boxShadow: isSelected ? '0 0 0 1px rgba(52, 178, 123, 0.35)' : 'none', + }} + enableResizing={isSelected} + disableDragging={!isSelected} + resizeHandleStyles={{ + topLeft: { + width: '12px', + height: '12px', + backgroundColor: isSelected ? 'white' : 'transparent', + border: isSelected ? '2px solid #34B27B' : 'none', + borderRadius: '50%', + left: '-6px', + top: '-6px', + cursor: 'nwse-resize', + }, + topRight: { + width: '12px', + height: '12px', + backgroundColor: isSelected ? 'white' : 'transparent', + border: isSelected ? '2px solid #34B27B' : 'none', + borderRadius: '50%', + right: '-6px', + top: '-6px', + cursor: 'nesw-resize', + }, + bottomLeft: { + width: '12px', + height: '12px', + backgroundColor: isSelected ? 'white' : 'transparent', + border: isSelected ? '2px solid #34B27B' : 'none', + borderRadius: '50%', + left: '-6px', + bottom: '-6px', + cursor: 'nesw-resize', + }, + bottomRight: { + width: '12px', + height: '12px', + backgroundColor: isSelected ? 'white' : 'transparent', + border: isSelected ? '2px solid #34B27B' : 'none', + borderRadius: '50%', + right: '-6px', + bottom: '-6px', + cursor: 'nwse-resize', + }, + }} + > +
+ {renderContent()} +
+
+ ); +} diff --git a/src/components/video-editor/AnnotationSettingsPanel.tsx b/src/components/video-editor/AnnotationSettingsPanel.tsx new file mode 100644 index 0000000..133e31b --- /dev/null +++ b/src/components/video-editor/AnnotationSettingsPanel.tsx @@ -0,0 +1,357 @@ +import { useState, useRef } from "react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Button } from "@/components/ui/button"; +import { Trash2, Type, Image as ImageIcon, Upload, Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight, ChevronDown } from "lucide-react"; +import { toast } from "sonner"; +import Colorful from '@uiw/react-color-colorful'; +import { hsvaToHex, hexToHsva } from '@uiw/color-convert'; +import type { AnnotationRegion, AnnotationType } from "./types"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; + +interface AnnotationSettingsPanelProps { + annotation: AnnotationRegion; + onContentChange: (content: string) => void; + onTypeChange: (type: AnnotationType) => void; + onStyleChange: (style: Partial) => void; + onDelete: () => void; +} + +const FONT_FAMILIES = [ + { value: 'system-ui, -apple-system, sans-serif', label: 'Classic' }, + { value: 'Georgia, serif', label: 'Editor' }, + { value: 'Impact, Arial Black, sans-serif', label: 'Strong' }, + { value: 'Courier New, monospace', label: 'Typewriter' }, + { value: 'Brush Script MT, cursive', label: 'Deco' }, + { value: 'Arial, sans-serif', label: 'Simple' }, + { value: 'Verdana, sans-serif', label: 'Modern' }, + { value: 'Trebuchet MS, sans-serif', label: 'Clean' }, +]; + +const FONT_SIZES = [12, 14, 16, 18, 20, 24, 28, 32, 36, 40, 48, 56, 64, 72, 80, 96, 128]; + +export function AnnotationSettingsPanel({ + annotation, + onContentChange, + onTypeChange, + onStyleChange, + onDelete, +}: AnnotationSettingsPanelProps) { + const fileInputRef = useRef(null); + const [textColorHsva, setTextColorHsva] = useState(hexToHsva(annotation.style.color)); + const [bgColorHsva, setBgColorHsva] = useState(hexToHsva(annotation.style.backgroundColor || '#00000000')); + + + + const handleImageUpload = (event: React.ChangeEvent) => { + const files = event.target.files; + if (!files || files.length === 0) return; + + const file = files[0]; + + // Validate file type + const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; + if (!validTypes.includes(file.type)) { + toast.error('Invalid file type', { + description: 'Please upload a JPG, PNG, GIF, or WebP image file.', + }); + event.target.value = ''; + return; + } + + const reader = new FileReader(); + + reader.onload = (e) => { + const dataUrl = e.target?.result as string; + if (dataUrl) { + onContentChange(dataUrl); + toast.success('Image uploaded successfully!'); + } + }; + + reader.onerror = () => { + toast.error('Failed to upload image', { + description: 'There was an error reading the file.', + }); + }; + + reader.readAsDataURL(file); + event.target.value = ''; + }; + + return ( +
+
+
+ Annotation Settings + + Active + +
+ + {/* Type Selector */} + onTypeChange(value as AnnotationType)} className="mb-6"> + + + + Text + + + + Image + + + + {/* Text Content */} + +
+ +