{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "image-editor-filters",
  "title": "Filters",
  "description": "LUT and color-matrix presets with a strength slider.",
  "dependencies": [
    "lucide-react",
    "photocn@^0.1.1"
  ],
  "registryDependencies": [
    "button",
    "field",
    "https://photocn.dev/r/image-editor-panel-header.json",
    "slider",
    "tooltip"
  ],
  "files": [
    {
      "path": "src/registry/image-editor/filters-panel.tsx",
      "content": "\"use client\";\n\nimport { useId, useState } from \"react\";\nimport { Loader2Icon } from \"lucide-react\";\n\nimport type { FilterOption } from \"photocn\";\nimport { filterPresets } from \"photocn/filters\";\nimport { useImageEditor } from \"photocn/react\";\n\nimport { PanelResetHeader } from \"./panel-header\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Field,\n  FieldGroup,\n  FieldLabel,\n} from \"@/components/ui/field\";\nimport { Slider } from \"@/components/ui/slider\";\nimport { TooltipProvider } from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface FiltersPanelValue {\n  label: string | null;\n  /** 0 = original photo, 1 = full filter. */\n  strength: number;\n}\n\nexport interface FiltersPanelOption {\n  label: string;\n  load: () => Promise<FilterOption>;\n}\n\nexport interface FiltersPanelSelection {\n  label: string;\n  opt: FilterOption;\n}\n\nexport interface FiltersPanelProps {\n  value: FiltersPanelValue;\n  options?: readonly FiltersPanelOption[];\n  disabled?: boolean;\n  className?: string;\n  onSelect: (next: FiltersPanelSelection | null) => void;\n  onStrengthChange?: (strength: number) => void;\n  onReset?: () => void;\n}\n\n/** Built-in presets: 9 LUT looks + 4 color matrices. */\nexport const filtersPanelDefaultOptions: readonly FiltersPanelOption[] = filterPresets;\n\nexport function FiltersPanel({\n  value,\n  options = filtersPanelDefaultOptions,\n  disabled = false,\n  className,\n  onSelect,\n  onStrengthChange,\n  onReset,\n}: FiltersPanelProps) {\n  const [loadingLabel, setLoadingLabel] = useState<string | null>(null);\n  const strengthSliderId = useId();\n\n  const isLoading = loadingLabel !== null;\n  const activeLabel = value.label;\n  const strengthPercent = Math.round(clamp01(value.strength) * 100);\n\n  const handleSelect = async (option: FiltersPanelOption) => {\n    if (disabled || isLoading) return;\n    setLoadingLabel(option.label);\n    try {\n      const opt = await option.load();\n      onSelect({ label: option.label, opt });\n    } finally {\n      setLoadingLabel(null);\n    }\n  };\n\n  const handleClear = () => {\n    if (disabled || isLoading) return;\n    onSelect(null);\n  };\n\n  const handleStrengthChange = (next: number | readonly number[]) => {\n    const raw = (typeof next === \"number\" ? next : next[0]) ?? strengthPercent;\n    onStrengthChange?.(clamp01(raw / 100));\n  };\n\n  return (\n    <TooltipProvider>\n      <section\n        aria-label=\"Filters\"\n        className={cn(\n          \"flex h-full min-w-0 flex-col bg-background text-foreground\",\n          className,\n        )}\n        data-slot=\"filters-panel\"\n      >\n        <PanelResetHeader\n          title=\"Filters\"\n          description=\"Film-style looks\"\n          onReset={onReset}\n          resetDisabled={disabled}\n        />\n\n        <FieldGroup className=\"min-h-0 flex-1 gap-5 overflow-auto px-4 py-4\">\n          <div\n            className=\"grid grid-cols-3 gap-2 @md/field-group:grid-cols-4\"\n            data-slot=\"filters-grid\"\n          >\n            <Button\n              aria-pressed={activeLabel === null}\n              className={cn(\n                \"h-9 px-2 text-xs\",\n                activeLabel === null && \"ring-2 ring-ring\",\n              )}\n              disabled={disabled || isLoading}\n              onClick={handleClear}\n              type=\"button\"\n              variant={activeLabel === null ? \"default\" : \"outline\"}\n            >\n              None\n            </Button>\n            {options.map((option) => {\n              const selected = activeLabel === option.label;\n              const loading = loadingLabel === option.label;\n              return (\n                <Button\n                  aria-pressed={selected}\n                  className={cn(\n                    \"h-9 px-2 text-xs capitalize\",\n                    selected && \"ring-2 ring-ring\",\n                  )}\n                  disabled={\n                    disabled || (isLoading && !loading)\n                  }\n                  key={option.label}\n                  onClick={() => handleSelect(option)}\n                  type=\"button\"\n                  variant={selected ? \"default\" : \"outline\"}\n                >\n                  {loading ? (\n                    <>\n                      <Loader2Icon\n                        aria-hidden=\"true\"\n                        className=\"animate-spin\"\n                      />\n                      <span className=\"sr-only\">Loading {option.label}</span>\n                    </>\n                  ) : (\n                    option.label\n                  )}\n                </Button>\n              );\n            })}\n          </div>\n\n          {activeLabel !== null ? (\n            <Field data-slot=\"filters-strength\" data-disabled={disabled}>\n              <div className=\"flex items-center justify-between gap-3\">\n                <FieldLabel htmlFor={strengthSliderId} id={`${strengthSliderId}-label`}>\n                  Strength\n                </FieldLabel>\n                <output className=\"shrink-0 text-xs tabular-nums text-muted-foreground\">\n                  {strengthPercent}%\n                </output>\n              </div>\n              <Slider\n                aria-labelledby={`${strengthSliderId}-label`}\n                disabled={disabled || !onStrengthChange}\n                id={strengthSliderId}\n                max={100}\n                min={0}\n                onValueChange={handleStrengthChange}\n                step={1}\n                value={[strengthPercent]}\n              />\n            </Field>\n          ) : null}\n        </FieldGroup>\n      </section>\n    </TooltipProvider>\n  );\n}\n\nfunction clamp01(value: number): number {\n  if (Number.isNaN(value)) return 0;\n  if (value < 0) return 0;\n  if (value > 1) return 1;\n  return value;\n}\n\nexport type ImageEditorFiltersProps = Partial<FiltersPanelProps>;\n\n/** LUT / color-matrix presets with a strength slider, wired to the nearest `<ImageEditorProvider>`. */\nexport function ImageEditorFilters(props: ImageEditorFiltersProps) {\n  const editor = useImageEditor();\n  return (\n    <FiltersPanel\n      disabled={editor.disabled}\n      onStrengthChange={editor.filters.setStrength}\n      onReset={editor.filters.reset}\n      onSelect={(next) =>\n        next\n          ? editor.patch(\"filters\", { opt: next.opt, mix: 0 })\n          : void editor.filters.select(null)\n      }\n      options={editor.filters.presets}\n      value={editor.filters.value}\n      {...props}\n    />\n  );\n}\n\n",
      "type": "registry:component",
      "target": "components/image-editor/filters-panel.tsx"
    }
  ],
  "categories": [
    "image-editor"
  ],
  "type": "registry:component"
}