{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "image-editor-adjustments",
  "title": "Adjustments",
  "description": "Light, color and effect sliders.",
  "dependencies": [
    "lucide-react",
    "photocn@^0.1.1"
  ],
  "registryDependencies": [
    "button",
    "field",
    "https://photocn.dev/r/image-editor-panel-header.json",
    "separator",
    "slider",
    "tabs",
    "tooltip"
  ],
  "files": [
    {
      "path": "src/registry/image-editor/adjustments-panel.tsx",
      "content": "\"use client\";\n\nimport { useId, type ReactElement, type ReactNode } from \"react\";\n\nimport { useControllableState } from \"photocn/hooks\";\nimport {\n  adjustDefaultValue,\n  useImageEditor,\n  type AdjustColorValue,\n  type AdjustEffectValue,\n  type AdjustLightValue,\n  type AdjustValue,\n} from \"photocn/react\";\nimport {\n  PaletteIcon,\n  RotateCcwIcon,\n  SparklesIcon,\n  SunMediumIcon,\n  type LucideIcon,\n} from \"lucide-react\";\n\nimport { PanelResetHeader } from \"./panel-header\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Field,\n  FieldContent,\n  FieldDescription,\n  FieldGroup,\n  FieldLabel,\n  FieldLegend,\n  FieldSet,\n  FieldTitle,\n} from \"@/components/ui/field\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Slider } from \"@/components/ui/slider\";\nimport {\n  Tabs,\n  TabsContent,\n  TabsList,\n  TabsTrigger,\n} from \"@/components/ui/tabs\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\n\nexport type AdjustmentLightValue = AdjustLightValue;\nexport type AdjustmentColorValue = AdjustColorValue;\nexport type AdjustmentEffectValue = AdjustEffectValue;\nexport type AdjustmentPanelValue = AdjustValue;\n\nexport type AdjustmentPanelSection = keyof AdjustmentPanelValue;\n\nexport type AdjustmentPanelControlKey<\n  TSection extends AdjustmentPanelSection,\n> = Extract<keyof AdjustmentPanelValue[TSection], string>;\n\nexport interface AdjustmentSliderConfig<TKey extends string = string> {\n  key: TKey;\n  label: string;\n  min: number;\n  max: number;\n  step: number;\n}\n\nexport interface AdjustmentPanelSectionConfig<\n  TSection extends AdjustmentPanelSection = AdjustmentPanelSection,\n> {\n  value: TSection;\n  label: string;\n  description: string;\n  icon: LucideIcon;\n  controls: readonly AdjustmentSliderConfig<AdjustmentPanelControlKey<TSection>>[];\n}\n\nexport interface AdjustmentPanelProps {\n  value: AdjustmentPanelValue;\n  activeSection?: AdjustmentPanelSection;\n  defaultSection?: AdjustmentPanelSection;\n  disabled?: boolean;\n  disabledSections?: Partial<Record<AdjustmentPanelSection, boolean>>;\n  className?: string;\n  onSectionChange?: (section: AdjustmentPanelSection) => void;\n  onValueChange?: (value: AdjustmentPanelValue) => void;\n  onLightsChange?: (lights: AdjustmentLightValue) => void;\n  onColorsChange?: (colors: AdjustmentColorValue) => void;\n  onEffectsChange?: (effects: AdjustmentEffectValue) => void;\n  onReset?: () => void;\n  onResetSection?: (section: AdjustmentPanelSection) => void;\n}\n\nexport const adjustmentPanelDefaultValue: AdjustmentPanelValue = adjustDefaultValue;\n\nexport const adjustmentPanelSections = [\n  {\n    value: \"lights\",\n    label: \"Light\",\n    description: \"Exposure, contrast, and tonal recovery\",\n    icon: SunMediumIcon,\n    controls: [\n      { key: \"brightness\", label: \"Brightness\", min: -1, max: 1, step: 0.01 },\n      { key: \"exposure\", label: \"Exposure\", min: -1, max: 1, step: 0.01 },\n      { key: \"gamma\", label: \"Gamma\", min: -1, max: 1, step: 0.01 },\n      { key: \"contrast\", label: \"Contrast\", min: -1, max: 1, step: 0.01 },\n      { key: \"shadows\", label: \"Shadows\", min: -1, max: 1, step: 0.01 },\n      { key: \"highlights\", label: \"Highlights\", min: -1, max: 1, step: 0.01 },\n      { key: \"bloom\", label: \"Bloom\", min: -1, max: 1, step: 0.01 },\n    ],\n  },\n  {\n    value: \"colors\",\n    label: \"Color\",\n    description: \"Temperature, tint, and color intensity\",\n    icon: PaletteIcon,\n    controls: [\n      { key: \"temperature\", label: \"Temperature\", min: -1, max: 1, step: 0.01 },\n      { key: \"tint\", label: \"Tint\", min: -1, max: 1, step: 0.01 },\n      { key: \"vibrance\", label: \"Vibrance\", min: -1, max: 1, step: 0.01 },\n      { key: \"saturation\", label: \"Saturation\", min: -1, max: 1, step: 0.01 },\n      { key: \"sepia\", label: \"Sepia\", min: -1, max: 1, step: 0.01 },\n    ],\n  },\n  {\n    value: \"effects\",\n    label: \"Effects\",\n    description: \"Texture, grain, and edge falloff\",\n    icon: SparklesIcon,\n    controls: [\n      { key: \"clarity\", label: \"Clarity\", min: -1, max: 1, step: 0.01 },\n      { key: \"noise\", label: \"Noise\", min: -1, max: 1, step: 0.01 },\n      { key: \"vignette\", label: \"Vignette\", min: -1, max: 1, step: 0.01 },\n    ],\n  },\n] as const satisfies readonly [\n  AdjustmentPanelSectionConfig<\"lights\">,\n  AdjustmentPanelSectionConfig<\"colors\">,\n  AdjustmentPanelSectionConfig<\"effects\">,\n];\n\nexport function AdjustmentPanel({\n  value,\n  activeSection: activeSectionProp,\n  defaultSection = \"lights\",\n  disabled = false,\n  disabledSections,\n  className,\n  onSectionChange,\n  onValueChange,\n  onLightsChange,\n  onColorsChange,\n  onEffectsChange,\n  onReset,\n  onResetSection,\n}: AdjustmentPanelProps) {\n  const [activeSection, handleSectionChange] = useControllableState<AdjustmentPanelSection>({\n    value: activeSectionProp,\n    defaultValue: defaultSection,\n    onChange: onSectionChange,\n  });\n  const canChangeLights = Boolean(onValueChange || onLightsChange);\n  const canChangeColors = Boolean(onValueChange || onColorsChange);\n  const canChangeEffects = Boolean(onValueChange || onEffectsChange);\n  const lightsDisabled = Boolean(\n    disabled || disabledSections?.lights || !canChangeLights,\n  );\n  const colorsDisabled = Boolean(\n    disabled || disabledSections?.colors || !canChangeColors,\n  );\n  const effectsDisabled = Boolean(\n    disabled || disabledSections?.effects || !canChangeEffects,\n  );\n\n  const updateLights = (lights: AdjustmentLightValue) => {\n    onLightsChange?.(lights);\n    onValueChange?.({ ...value, lights });\n  };\n\n  const updateColors = (colors: AdjustmentColorValue) => {\n    onColorsChange?.(colors);\n    onValueChange?.({ ...value, colors });\n  };\n\n  const updateEffects = (effects: AdjustmentEffectValue) => {\n    onEffectsChange?.(effects);\n    onValueChange?.({ ...value, effects });\n  };\n\n  return (\n    <TooltipProvider>\n      <section\n        aria-label=\"Adjustments\"\n        className={cn(\n          \"flex h-full min-w-0 flex-col bg-background text-foreground\",\n          className,\n        )}\n        data-slot=\"adjustment-panel\"\n      >\n        <PanelResetHeader\n          title=\"Adjustments\"\n          description=\"Light, color, and effects\"\n          onReset={onReset}\n          resetDisabled={disabled}\n        />\n\n        <Tabs\n          className=\"min-h-0 flex-1 gap-0\"\n          onValueChange={(section) =>\n            handleSectionChange(section as AdjustmentPanelSection)\n          }\n          value={activeSection}\n        >\n          <div className=\"px-4 py-3\">\n            <TabsList className=\"grid w-full grid-cols-3\" variant=\"line\">\n              {adjustmentPanelSections.map((section) => (\n                <AdjustmentTabTrigger\n                  disabled={disabled}\n                  key={section.value}\n                  section={section}\n                />\n              ))}\n            </TabsList>\n          </div>\n          <Separator />\n\n          <div className=\"min-h-0 flex-1 overflow-auto px-4 py-4\">\n            <TabsContent className=\"m-0\" value=\"lights\">\n              <AdjustmentSectionFields\n                controls={adjustmentPanelSections[0].controls}\n                description={adjustmentPanelSections[0].description}\n                disabled={lightsDisabled}\n                onChange={updateLights}\n                onReset={\n                  onResetSection\n                    ? () => onResetSection(\"lights\")\n                    : undefined\n                }\n                resetDisabled={lightsDisabled || !onResetSection}\n                title={adjustmentPanelSections[0].label}\n                value={value.lights}\n              />\n            </TabsContent>\n\n            <TabsContent className=\"m-0\" value=\"colors\">\n              <AdjustmentSectionFields\n                controls={adjustmentPanelSections[1].controls}\n                description={adjustmentPanelSections[1].description}\n                disabled={colorsDisabled}\n                onChange={updateColors}\n                onReset={\n                  onResetSection\n                    ? () => onResetSection(\"colors\")\n                    : undefined\n                }\n                resetDisabled={colorsDisabled || !onResetSection}\n                title={adjustmentPanelSections[1].label}\n                value={value.colors}\n              />\n            </TabsContent>\n\n            <TabsContent className=\"m-0\" value=\"effects\">\n              <AdjustmentSectionFields\n                controls={adjustmentPanelSections[2].controls}\n                description={adjustmentPanelSections[2].description}\n                disabled={effectsDisabled}\n                onChange={updateEffects}\n                onReset={\n                  onResetSection\n                    ? () => onResetSection(\"effects\")\n                    : undefined\n                }\n                resetDisabled={effectsDisabled || !onResetSection}\n                title={adjustmentPanelSections[2].label}\n                value={value.effects}\n              />\n            </TabsContent>\n          </div>\n        </Tabs>\n      </section>\n    </TooltipProvider>\n  );\n}\n\nexport type ImageEditorAdjustmentsProps = Partial<AdjustmentPanelProps>;\n\n/** Light, color and effect sliders wired to the nearest `<ImageEditorProvider>`. */\nexport function ImageEditorAdjustments(props: ImageEditorAdjustmentsProps) {\n  const editor = useImageEditor();\n  return (\n    <AdjustmentPanel\n      disabled={editor.disabled}\n      onColorsChange={editor.adjust.setColors}\n      onEffectsChange={editor.adjust.setEffects}\n      onLightsChange={editor.adjust.setLights}\n      onReset={editor.adjust.reset}\n      onResetSection={editor.adjust.resetSection}\n      value={editor.adjust.value}\n      {...props}\n    />\n  );\n}\n\nfunction AdjustmentTabTrigger({\n  disabled,\n  section,\n}: {\n  disabled?: boolean;\n  section: (typeof adjustmentPanelSections)[number];\n}) {\n  const Icon = section.icon;\n\n  // Native `title` for hover hints; a tooltip trigger around the tab would\n  // add its own state attributes to it.\n  return (\n    <TabsTrigger\n      aria-label={section.label}\n      disabled={disabled}\n      title={section.description}\n      value={section.value}\n    >\n      <Icon aria-hidden=\"true\" />\n      <span>{section.label}</span>\n    </TabsTrigger>\n  );\n}\n\nfunction AdjustmentSectionFields<\n  TKey extends string,\n  TValue extends Record<TKey, number>,\n>({\n  controls,\n  description,\n  disabled,\n  onChange,\n  onReset,\n  resetDisabled,\n  title,\n  value,\n}: {\n  controls: readonly AdjustmentSliderConfig<TKey>[];\n  description: string;\n  disabled?: boolean;\n  onChange?: (value: TValue) => void;\n  onReset?: () => void;\n  resetDisabled?: boolean;\n  title: string;\n  value: TValue;\n}) {\n  return (\n    <FieldSet className=\"gap-4\">\n      <FieldLegend className=\"sr-only\">{title}</FieldLegend>\n      <div className=\"flex items-start justify-between gap-3\">\n        <div className=\"min-w-0\">\n          <FieldTitle>{title}</FieldTitle>\n          <FieldDescription>{description}</FieldDescription>\n        </div>\n        <IconButtonTooltip label={`Reset ${title.toLowerCase()}`}>\n          <Button\n            aria-label={`Reset ${title.toLowerCase()}`}\n            disabled={resetDisabled}\n            onClick={onReset}\n            size=\"icon-sm\"\n            type=\"button\"\n            variant=\"ghost\"\n          >\n            <RotateCcwIcon aria-hidden=\"true\" data-icon=\"inline-start\" />\n          </Button>\n        </IconButtonTooltip>\n      </div>\n      <FieldGroup className=\"gap-4\">\n        {controls.map((control) => (\n          <AdjustmentSlider\n            disabled={disabled}\n            key={control.key}\n            label={control.label}\n            max={control.max}\n            min={control.min}\n            onValueChange={(nextValue) =>\n              onChange?.({ ...value, [control.key]: nextValue } as TValue)\n            }\n            step={control.step}\n            value={value[control.key]}\n          />\n        ))}\n      </FieldGroup>\n    </FieldSet>\n  );\n}\n\nfunction AdjustmentSlider({\n  disabled,\n  label,\n  max,\n  min,\n  onValueChange,\n  step,\n  value,\n}: {\n  disabled?: boolean;\n  label: string;\n  max: number;\n  min: number;\n  onValueChange?: (value: number) => void;\n  step: number;\n  value: number;\n}) {\n  const sliderId = useId();\n  const labelId = `${sliderId}-label`;\n\n  return (\n    <Field data-disabled={disabled} data-slot=\"adjustment-slider\">\n      <div className=\"flex items-center justify-between gap-3\">\n        <FieldLabel id={labelId}>{label}</FieldLabel>\n        <output className=\"shrink-0 text-xs tabular-nums text-muted-foreground\">\n          {formatAdjustmentValue(value)}\n        </output>\n      </div>\n      <FieldContent>\n        <Slider\n          aria-labelledby={labelId}\n          disabled={disabled}\n          id={sliderId}\n          max={max}\n          min={min}\n          onValueChange={(nextValue) =>\n            onValueChange?.(readSliderValue(nextValue, value))\n          }\n          step={step}\n          value={[value]}\n        />\n      </FieldContent>\n    </Field>\n  );\n}\n\nfunction IconButtonTooltip({\n  children,\n  label,\n}: {\n  children: ReactElement;\n  label: string;\n}) {\n  return (\n    <Tooltip>\n      <TooltipTrigger render={children} />\n      <TooltipContent>{label}</TooltipContent>\n    </Tooltip>\n  );\n}\n\nfunction readSliderValue(nextValue: number | readonly number[], fallback: number): number {\n  return (typeof nextValue === \"number\" ? nextValue : nextValue[0]) ?? fallback;\n}\n\nfunction formatAdjustmentValue(value: number): string {\n  const rounded = Math.round(value * 100) / 100;\n\n  if (rounded > 0) {\n    return `+${rounded.toFixed(2)}`;\n  }\n\n  return rounded.toFixed(2);\n}\n",
      "type": "registry:component",
      "target": "components/image-editor/adjustments-panel.tsx"
    }
  ],
  "categories": [
    "image-editor"
  ],
  "type": "registry:component"
}