{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "image-editor-blur",
  "title": "Blur",
  "description": "Bokeh and gaussian lens blur with a draggable focus point.",
  "dependencies": [
    "lucide-react",
    "photocn@^0.1.1"
  ],
  "registryDependencies": [
    "field",
    "https://photocn.dev/r/image-editor-panel-header.json",
    "separator",
    "slider",
    "tabs",
    "tooltip"
  ],
  "files": [
    {
      "path": "src/registry/image-editor/blur-panel.tsx",
      "content": "\"use client\";\n\nimport { useId, type ReactNode } from \"react\";\nimport { ApertureIcon, CircleDotIcon } from \"lucide-react\";\n\nimport { useControllableState } from \"photocn/hooks\";\nimport { useImageEditor } from \"photocn/react\";\n\nimport { PanelResetHeader } from \"./panel-header\";\nimport {\n  Field,\n  FieldContent,\n  FieldGroup,\n  FieldLabel,\n  FieldLegend,\n  FieldSet,\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  TooltipProvider,\n} from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface BlurPanelValue {\n  bokehStrength: number;\n  bokehLensOut: number;\n  gaussianStrength: number;\n  gaussianLensOut: number;\n  centerX: number;\n  centerY: number;\n}\n\nexport type BlurPanelKind = \"bokeh\" | \"gaussian\";\n\nexport interface BlurPanelProps {\n  value: BlurPanelValue;\n  activeKind?: BlurPanelKind;\n  disabled?: boolean;\n  className?: string;\n  /** Called transient on slider drag; the parent uses `commitPatchSectionTransient` so drags coalesce into one undo step. */\n  onChange: (next: BlurPanelValue) => void;\n  onActiveKindChange?: (kind: BlurPanelKind) => void;\n  onReset?: () => void;\n}\n\nexport const blurPanelDefaultValue: BlurPanelValue = {\n  bokehStrength: 0,\n  bokehLensOut: 0.5,\n  gaussianStrength: 0,\n  gaussianLensOut: 0.5,\n  centerX: 0.5,\n  centerY: 0.5,\n};\n\nexport function isBlurPanelValueDefault(value: BlurPanelValue): boolean {\n  return (\n    value.bokehStrength === blurPanelDefaultValue.bokehStrength &&\n    value.bokehLensOut === blurPanelDefaultValue.bokehLensOut &&\n    value.gaussianStrength === blurPanelDefaultValue.gaussianStrength &&\n    value.gaussianLensOut === blurPanelDefaultValue.gaussianLensOut &&\n    value.centerX === blurPanelDefaultValue.centerX &&\n    value.centerY === blurPanelDefaultValue.centerY\n  );\n}\n\ninterface BlurKindConfig {\n  value: BlurPanelKind;\n  label: string;\n  description: string;\n  icon: typeof ApertureIcon;\n  strengthKey: \"bokehStrength\" | \"gaussianStrength\";\n  lensOutKey: \"bokehLensOut\" | \"gaussianLensOut\";\n  strengthLabel: string;\n  lensOutLabel: string;\n}\n\nconst blurKindConfigs = [\n  {\n    value: \"bokeh\",\n    label: \"Bokeh\",\n    description: \"Lens-style circular bokeh\",\n    icon: ApertureIcon,\n    strengthKey: \"bokehStrength\",\n    lensOutKey: \"bokehLensOut\",\n    strengthLabel: \"Strength\",\n    lensOutLabel: \"Radius\",\n  },\n  {\n    value: \"gaussian\",\n    label: \"Gaussian\",\n    description: \"Soft gaussian falloff\",\n    icon: CircleDotIcon,\n    strengthKey: \"gaussianStrength\",\n    lensOutKey: \"gaussianLensOut\",\n    strengthLabel: \"Strength\",\n    lensOutLabel: \"Radius\",\n  },\n] as const satisfies readonly BlurKindConfig[];\n\nexport function BlurPanel({\n  value,\n  activeKind,\n  disabled = false,\n  className,\n  onChange,\n  onActiveKindChange,\n  onReset,\n}: BlurPanelProps) {\n  const [currentKind, handleKindChange] = useControllableState<BlurPanelKind>({\n    value: activeKind,\n    defaultValue: \"bokeh\",\n    onChange: onActiveKindChange,\n  });\n\n  const isDefault = isBlurPanelValueDefault(value);\n  const resetDisabled = disabled || !onReset || isDefault;\n\n  const updateValue = <TKey extends keyof BlurPanelValue>(\n    key: TKey,\n    next: BlurPanelValue[TKey],\n  ) => {\n    onChange({ ...value, [key]: next });\n  };\n\n  return (\n    <TooltipProvider>\n      <section\n        aria-label=\"Blur\"\n        className={cn(\n          \"flex h-full min-w-0 flex-col bg-background text-foreground\",\n          className,\n        )}\n        data-slot=\"blur-panel\"\n      >\n        <PanelResetHeader\n          title=\"Blur\"\n          description=\"Bokeh and gaussian blur\"\n          onReset={onReset}\n          resetDisabled={resetDisabled}\n        />\n\n        <Tabs\n          className=\"min-h-0 flex-1 gap-0\"\n          onValueChange={(kind) => handleKindChange(kind as BlurPanelKind)}\n          value={currentKind}\n        >\n          <div className=\"px-4 py-3\">\n            <TabsList className=\"grid w-full grid-cols-2\" variant=\"line\">\n              {blurKindConfigs.map((kind) => (\n                <BlurKindTabTrigger\n                  disabled={disabled}\n                  key={kind.value}\n                  kind={kind}\n                />\n              ))}\n            </TabsList>\n          </div>\n          <Separator />\n\n          <FieldGroup className=\"min-h-0 flex-1 gap-5 overflow-auto px-4 py-4\">\n            {blurKindConfigs.map((kind) => (\n              <TabsContent\n                className=\"m-0 flex flex-col gap-4\"\n                key={kind.value}\n                value={kind.value}\n              >\n                <BlurSlider\n                  disabled={disabled}\n                  label={kind.strengthLabel}\n                  onValueChange={(next) =>\n                    updateValue(kind.strengthKey, next)\n                  }\n                  value={value[kind.strengthKey]}\n                />\n                <BlurSlider\n                  disabled={disabled}\n                  label={kind.lensOutLabel}\n                  onValueChange={(next) => updateValue(kind.lensOutKey, next)}\n                  value={value[kind.lensOutKey]}\n                />\n              </TabsContent>\n            ))}\n\n            <Separator />\n\n            <PanelSection title=\"Center\">\n              <BlurSlider\n                disabled={disabled}\n                label=\"X\"\n                onValueChange={(next) => updateValue(\"centerX\", next)}\n                value={value.centerX}\n              />\n              <BlurSlider\n                disabled={disabled}\n                label=\"Y\"\n                onValueChange={(next) => updateValue(\"centerY\", next)}\n                value={value.centerY}\n              />\n            </PanelSection>\n          </FieldGroup>\n        </Tabs>\n      </section>\n    </TooltipProvider>\n  );\n}\n\nfunction BlurKindTabTrigger({\n  disabled,\n  kind,\n}: {\n  disabled?: boolean;\n  kind: (typeof blurKindConfigs)[number];\n}) {\n  const Icon = kind.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={kind.label}\n      disabled={disabled}\n      title={kind.description}\n      value={kind.value}\n    >\n      <Icon aria-hidden=\"true\" />\n      <span>{kind.label}</span>\n    </TabsTrigger>\n  );\n}\n\nfunction PanelSection({\n  children,\n  title,\n}: {\n  children: ReactNode;\n  title: string;\n}) {\n  return (\n    <FieldSet className=\"gap-3\" data-slot=\"blur-section\">\n      <FieldLegend\n        className=\"mb-0 text-xs font-semibold uppercase text-muted-foreground\"\n        variant=\"label\"\n      >\n        {title}\n      </FieldLegend>\n      <FieldGroup className=\"gap-3\">{children}</FieldGroup>\n    </FieldSet>\n  );\n}\n\nfunction BlurSlider({\n  disabled,\n  label,\n  onValueChange,\n  value,\n}: {\n  disabled?: boolean;\n  label: string;\n  onValueChange: (value: number) => void;\n  value: number;\n}) {\n  const sliderId = useId();\n  const labelId = `${sliderId}-label`;\n\n  return (\n    <Field data-disabled={disabled} data-slot=\"blur-slider\">\n      <div className=\"flex items-center justify-between gap-3\">\n        <FieldLabel htmlFor={sliderId} id={labelId}>\n          {label}\n        </FieldLabel>\n        <output className=\"shrink-0 text-xs tabular-nums text-muted-foreground\">\n          {formatPercent(value)}\n        </output>\n      </div>\n      <FieldContent>\n        <Slider\n          aria-labelledby={labelId}\n          disabled={disabled}\n          id={sliderId}\n          max={100}\n          min={0}\n          onValueChange={(next) =>\n            onValueChange(fromSliderPercent(next, value))\n          }\n          step={1}\n          value={[toSliderPercent(value)]}\n        />\n      </FieldContent>\n    </Field>\n  );\n}\n\nfunction toSliderPercent(value: number): number {\n  return Math.round(clamp01(value) * 100);\n}\n\nfunction fromSliderPercent(next: number | readonly number[], fallback: number): number {\n  const raw = typeof next === \"number\" ? next : next[0];\n  if (raw === undefined) return fallback;\n  return clamp01(raw / 100);\n}\n\nfunction clamp01(value: number): number {\n  if (value < 0) return 0;\n  if (value > 1) return 1;\n  return value;\n}\n\nfunction formatPercent(value: number): string {\n  return `${Math.round(clamp01(value) * 100)}%`;\n}\n\nexport type ImageEditorBlurProps = Partial<BlurPanelProps>;\n\n/** Bokeh / gaussian lens blur wired to the nearest `<ImageEditorProvider>`. */\nexport function ImageEditorBlur(props: ImageEditorBlurProps) {\n  const editor = useImageEditor();\n  return (\n    <BlurPanel\n      disabled={editor.disabled}\n      onChange={editor.blur.set}\n      onReset={editor.blur.reset}\n      value={editor.blur.value}\n      {...props}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/image-editor/blur-panel.tsx"
    }
  ],
  "categories": [
    "image-editor"
  ],
  "type": "registry:component"
}