{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "image-editor-blend",
  "title": "Blend",
  "description": "Blend a second image into the photo.",
  "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/blend-panel.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { ImagePlusIcon, XIcon } from \"lucide-react\";\n\nimport { decodeImageInput } from \"photocn/dom\";\nimport { useImageEditor } from \"photocn/react\";\n\nimport { PanelResetHeader } from \"./panel-header\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Field,\n  FieldContent,\n  FieldGroup,\n  FieldLabel,\n} from \"@/components/ui/field\";\nimport { Slider } from \"@/components/ui/slider\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface BlenderPanelValue {\n  blendMix: number;\n}\n\nexport interface BlenderPanelProps {\n  value: BlenderPanelValue;\n  disabled?: boolean;\n  className?: string;\n  onBlendMapChange: (image: HTMLImageElement | null) => void;\n  onMixChange?: (mix: number) => void;\n  onReset?: () => void;\n  onError?: (error: unknown) => void;\n}\n\nconst DEFAULT_MIX = 0.5;\n\nexport function BlenderPanel({\n  value,\n  disabled = false,\n  className,\n  onBlendMapChange,\n  onMixChange,\n  onReset,\n  onError,\n}: BlenderPanelProps) {\n  const fileInputRef = useRef<HTMLInputElement | null>(null);\n  const sliderId = useId();\n  const labelId = `${sliderId}-label`;\n  const [name, setName] = useState<string | null>(null);\n  const [preview, setPreview] = useState<string | null>(null);\n\n  // Revoke the captured object URL when `preview` changes (cleanup runs with\n  // the prior closure) and on unmount.\n  useEffect(() => {\n    if (!preview) return;\n    return () => URL.revokeObjectURL(preview);\n  }, [preview]);\n\n  const hasBlend = name !== null;\n  const isDirty = hasBlend || value.blendMix !== DEFAULT_MIX;\n  const resetDisabled = disabled || !onReset || !isDirty;\n  const sliderDisabled = disabled || !hasBlend || !onMixChange;\n\n  const openFilePicker = () => {\n    fileInputRef.current?.click();\n  };\n\n  const setBlend = (next: { name: string; preview: string } | null) => {\n    setPreview(next?.preview ?? null);\n    setName(next?.name ?? null);\n  };\n\n  const handleSelectFile = async (file: File) => {\n    try {\n      const result = await decodeImageInput(file);\n      const nextPreview = URL.createObjectURL(file);\n      setBlend({ name: file.name, preview: nextPreview });\n      onBlendMapChange(result.image);\n    } catch (error) {\n      onError?.(error);\n    }\n  };\n\n  const handleClear = () => {\n    setBlend(null);\n    onBlendMapChange(null);\n  };\n\n  const handleReset = () => {\n    setBlend(null);\n    onReset?.();\n  };\n\n  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {\n    const file = event.target.files?.[0];\n    // Reset the input so re-selecting the same file fires onChange again.\n    event.target.value = \"\";\n    if (file) void handleSelectFile(file);\n  };\n\n  return (\n    <TooltipProvider>\n      <section\n        aria-label=\"Blender\"\n        className={cn(\n          \"flex h-full min-w-0 flex-col bg-background text-foreground\",\n          className,\n        )}\n        data-slot=\"blender-panel\"\n      >\n        <PanelResetHeader\n          title=\"Blend\"\n          description=\"Mix a second image into the photo\"\n          onReset={handleReset}\n          resetDisabled={resetDisabled}\n        />\n\n        <input\n          accept=\"image/*\"\n          className=\"sr-only\"\n          onChange={handleFileChange}\n          ref={fileInputRef}\n          tabIndex={-1}\n          type=\"file\"\n        />\n\n        <FieldGroup className=\"min-h-0 flex-1 gap-5 overflow-auto px-4 py-4\">\n          {hasBlend ? (\n            <BlendImageSummary\n              disabled={disabled}\n              name={name ?? \"\"}\n              onChange={openFilePicker}\n              onClear={handleClear}\n              preview={preview}\n            />\n          ) : (\n            <BlendImageEmpty\n              disabled={disabled}\n              onChoose={openFilePicker}\n            />\n          )}\n\n          <Field data-disabled={sliderDisabled} data-slot=\"blender-mix\">\n            <div className=\"flex items-center justify-between gap-3\">\n              <FieldLabel htmlFor={sliderId} id={labelId}>\n                Blend mix\n              </FieldLabel>\n              <output className=\"shrink-0 text-xs tabular-nums text-muted-foreground\">\n                {formatMix(value.blendMix)}\n              </output>\n            </div>\n            <FieldContent>\n              <Slider\n                aria-labelledby={labelId}\n                disabled={sliderDisabled}\n                id={sliderId}\n                max={1}\n                min={0}\n                onValueChange={(next) =>\n                  onMixChange?.(readSliderValue(next, value.blendMix))\n                }\n                step={0.01}\n                value={[value.blendMix]}\n              />\n            </FieldContent>\n          </Field>\n        </FieldGroup>\n      </section>\n    </TooltipProvider>\n  );\n}\n\nfunction BlendImageEmpty({\n  disabled,\n  onChoose,\n}: {\n  disabled?: boolean;\n  onChoose: () => void;\n}) {\n  return (\n    <div\n      className={cn(\n        \"flex flex-col items-center justify-center gap-2 rounded-md border border-dashed border-border px-4 py-8 text-center\",\n        disabled && \"opacity-60\",\n      )}\n      data-slot=\"blender-empty\"\n    >\n      <ImagePlusIcon\n        aria-hidden=\"true\"\n        className=\"size-6 text-muted-foreground\"\n      />\n      <p className=\"text-sm font-medium\">Choose blend image</p>\n      <p className=\"text-xs text-muted-foreground\">\n        Pick an image to blend with the current photo\n      </p>\n      <Button\n        className=\"mt-1\"\n        disabled={disabled}\n        onClick={onChoose}\n        size=\"sm\"\n        type=\"button\"\n      >\n        Choose blend image\n      </Button>\n    </div>\n  );\n}\n\nfunction BlendImageSummary({\n  disabled,\n  name,\n  onChange,\n  onClear,\n  preview,\n}: {\n  disabled?: boolean;\n  name: string;\n  onChange: () => void;\n  onClear?: () => void;\n  preview: string | null;\n}) {\n  return (\n    <div\n      className=\"flex items-center gap-3 rounded-md border border-border bg-muted/30 p-2\"\n      data-slot=\"blender-summary\"\n    >\n      <div className=\"flex size-12 shrink-0 items-center justify-center overflow-hidden rounded bg-muted text-muted-foreground\">\n        {preview ? (\n          <img\n            alt=\"\"\n            className=\"size-full object-cover\"\n            src={preview}\n          />\n        ) : (\n          <ImagePlusIcon aria-hidden=\"true\" className=\"size-5\" />\n        )}\n      </div>\n      <div className=\"min-w-0 flex-1\">\n        <p className=\"truncate text-sm font-medium\" title={name}>\n          {name}\n        </p>\n        <p className=\"text-xs text-muted-foreground\">Blend image</p>\n      </div>\n      <Tooltip>\n        <TooltipTrigger\n          render={\n            <Button\n              disabled={disabled}\n              onClick={onChange}\n              size=\"sm\"\n              type=\"button\"\n              variant=\"ghost\"\n            >\n              Change\n            </Button>\n          }\n        />\n        <TooltipContent>Pick a different blend image</TooltipContent>\n      </Tooltip>\n      {onClear ? (\n        <Tooltip>\n          <TooltipTrigger\n            render={\n              <Button\n                aria-label=\"Clear blend image\"\n                disabled={disabled}\n                onClick={onClear}\n                size=\"icon-sm\"\n                type=\"button\"\n                variant=\"ghost\"\n              >\n                <XIcon aria-hidden=\"true\" />\n              </Button>\n            }\n          />\n          <TooltipContent>Clear blend image</TooltipContent>\n        </Tooltip>\n      ) : null}\n    </div>\n  );\n}\n\nfunction readSliderValue(nextValue: number | readonly number[], fallback: number): number {\n  return (typeof nextValue === \"number\" ? nextValue : nextValue[0]) ?? fallback;\n}\n\nfunction formatMix(value: number): string {\n  return `${Math.round(value * 100)}%`;\n}\n\nexport type ImageEditorBlendProps = Partial<BlenderPanelProps>;\n\n/** Blend a second image into the photo, wired to the nearest `<ImageEditorProvider>`. */\nexport function ImageEditorBlend(props: ImageEditorBlendProps) {\n  const editor = useImageEditor();\n  return (\n    <BlenderPanel\n      disabled={editor.disabled}\n      // Remount per image so the picked blend file doesn't leak across photos.\n      key={editor.imageSrc ?? \"no-image\"}\n      onBlendMapChange={editor.blend.setImage}\n      onMixChange={editor.blend.setMix}\n      onReset={editor.blend.reset}\n      value={editor.blend.value}\n      {...props}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/image-editor/blend-panel.tsx"
    }
  ],
  "categories": [
    "image-editor"
  ],
  "type": "registry:component"
}