diff --git a/app/playground/page.tsx b/app/playground/page.tsx
index 4b33cb2..485a8af 100644
--- a/app/playground/page.tsx
+++ b/app/playground/page.tsx
@@ -4,7 +4,7 @@ import { useState } from 'react';
import { ColorPicker } from '@/components/color/ColorPicker';
import { ColorDisplay } from '@/components/color/ColorDisplay';
import { ColorInfo } from '@/components/color/ColorInfo';
-import { Button } from '@/components/ui/button';
+import { ManipulationPanel } from '@/components/tools/ManipulationPanel';
import { useColorInfo } from '@/lib/api/queries';
import { Loader2 } from 'lucide-react';
@@ -65,27 +65,8 @@ export default function PlaygroundPage() {
-
Quick Actions
-
-
-
-
-
-
-
-
+
Color Manipulation
+
diff --git a/components/tools/ManipulationPanel.tsx b/components/tools/ManipulationPanel.tsx
new file mode 100644
index 0000000..54f910e
--- /dev/null
+++ b/components/tools/ManipulationPanel.tsx
@@ -0,0 +1,231 @@
+'use client';
+
+import { useState } from 'react';
+import { Slider } from '@/components/ui/slider';
+import { Button } from '@/components/ui/button';
+import {
+ useLighten,
+ useDarken,
+ useSaturate,
+ useDesaturate,
+ useRotate,
+ useComplement
+} from '@/lib/api/queries';
+import { toast } from 'sonner';
+
+interface ManipulationPanelProps {
+ color: string;
+ onColorChange: (color: string) => void;
+}
+
+export function ManipulationPanel({ color, onColorChange }: ManipulationPanelProps) {
+ const [lightenAmount, setLightenAmount] = useState(0.2);
+ const [darkenAmount, setDarkenAmount] = useState(0.2);
+ const [saturateAmount, setSaturateAmount] = useState(0.2);
+ const [desaturateAmount, setDesaturateAmount] = useState(0.2);
+ const [rotateAmount, setRotateAmount] = useState(30);
+
+ const lightenMutation = useLighten();
+ const darkenMutation = useDarken();
+ const saturateMutation = useSaturate();
+ const desaturateMutation = useDesaturate();
+ const rotateMutation = useRotate();
+ const complementMutation = useComplement();
+
+ const handleLighten = async () => {
+ try {
+ const result = await lightenMutation.mutateAsync({
+ colors: [color],
+ amount: lightenAmount,
+ });
+ if (result.results[0]) {
+ onColorChange(result.results[0].output);
+ toast.success(`Lightened by ${(lightenAmount * 100).toFixed(0)}%`);
+ }
+ } catch (error) {
+ toast.error('Failed to lighten color');
+ }
+ };
+
+ const handleDarken = async () => {
+ try {
+ const result = await darkenMutation.mutateAsync({
+ colors: [color],
+ amount: darkenAmount,
+ });
+ if (result.results[0]) {
+ onColorChange(result.results[0].output);
+ toast.success(`Darkened by ${(darkenAmount * 100).toFixed(0)}%`);
+ }
+ } catch (error) {
+ toast.error('Failed to darken color');
+ }
+ };
+
+ const handleSaturate = async () => {
+ try {
+ const result = await saturateMutation.mutateAsync({
+ colors: [color],
+ amount: saturateAmount,
+ });
+ if (result.results[0]) {
+ onColorChange(result.results[0].output);
+ toast.success(`Saturated by ${(saturateAmount * 100).toFixed(0)}%`);
+ }
+ } catch (error) {
+ toast.error('Failed to saturate color');
+ }
+ };
+
+ const handleDesaturate = async () => {
+ try {
+ const result = await desaturateMutation.mutateAsync({
+ colors: [color],
+ amount: desaturateAmount,
+ });
+ if (result.results[0]) {
+ onColorChange(result.results[0].output);
+ toast.success(`Desaturated by ${(desaturateAmount * 100).toFixed(0)}%`);
+ }
+ } catch (error) {
+ toast.error('Failed to desaturate color');
+ }
+ };
+
+ const handleRotate = async () => {
+ try {
+ const result = await rotateMutation.mutateAsync({
+ colors: [color],
+ amount: rotateAmount,
+ });
+ if (result.results[0]) {
+ onColorChange(result.results[0].output);
+ toast.success(`Rotated hue by ${rotateAmount}°`);
+ }
+ } catch (error) {
+ toast.error('Failed to rotate hue');
+ }
+ };
+
+ const handleComplement = async () => {
+ try {
+ const result = await complementMutation.mutateAsync([color]);
+ if (result.results[0]) {
+ onColorChange(result.results[0].output);
+ toast.success('Generated complementary color');
+ }
+ } catch (error) {
+ toast.error('Failed to generate complement');
+ }
+ };
+
+ const isLoading =
+ lightenMutation.isPending ||
+ darkenMutation.isPending ||
+ saturateMutation.isPending ||
+ desaturateMutation.isPending ||
+ rotateMutation.isPending ||
+ complementMutation.isPending;
+
+ return (
+
+ {/* Lighten */}
+
+ setLightenAmount(parseFloat(e.target.value))}
+ suffix="%"
+ showValue
+ />
+
+
+
+ {/* Darken */}
+
+ setDarkenAmount(parseFloat(e.target.value))}
+ suffix="%"
+ showValue
+ />
+
+
+
+ {/* Saturate */}
+
+ setSaturateAmount(parseFloat(e.target.value))}
+ suffix="%"
+ showValue
+ />
+
+
+
+ {/* Desaturate */}
+
+ setDesaturateAmount(parseFloat(e.target.value))}
+ suffix="%"
+ showValue
+ />
+
+
+
+ {/* Rotate Hue */}
+
+ setRotateAmount(parseInt(e.target.value))}
+ suffix="°"
+ showValue
+ />
+
+
+
+ {/* Quick Actions */}
+
+
Quick Actions
+
+
+
+ );
+}
diff --git a/components/ui/slider.tsx b/components/ui/slider.tsx
new file mode 100644
index 0000000..f2da193
--- /dev/null
+++ b/components/ui/slider.tsx
@@ -0,0 +1,65 @@
+'use client';
+
+import * as React from 'react';
+import { cn } from '@/lib/utils/cn';
+
+export interface SliderProps extends Omit, 'type'> {
+ label?: string;
+ showValue?: boolean;
+ suffix?: string;
+}
+
+const Slider = React.forwardRef(
+ ({ className, label, showValue = true, suffix = '', ...props }, ref) => {
+ const [value, setValue] = React.useState(props.value || props.defaultValue || 0);
+
+ const handleChange = (e: React.ChangeEvent) => {
+ setValue(e.target.value);
+ props.onChange?.(e);
+ };
+
+ return (
+
+ {(label || showValue) && (
+
+ {label && (
+
+ )}
+ {showValue && (
+
+ {value}
+ {suffix}
+
+ )}
+
+ )}
+
+
+ );
+ }
+);
+
+Slider.displayName = 'Slider';
+
+export { Slider };