{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "fill-picker-engine",
  "type": "registry:ui",
  "title": "Amplo Fill Picker Engine",
  "description": "Dialect-agnostic core of the Amplo fill picker: the solid/gradient mode state (fill context + `useFillPicker`) and the FillPicker.Root/Tabs/Tab/Pane parts both variants share. Installed automatically as a dependency of `fill-picker` (Base UI) and `fill-picker-radix`; you do not normally add it directly.",
  "version": "2.0.0",
  "categories": [
    "color",
    "input",
    "design",
    "accessibility"
  ],
  "dependencies": [],
  "registryDependencies": [
    "utils",
    "https://amplo.ale.design/r/gradient-picker-engine.json"
  ],
  "files": [
    {
      "path": "registry/new-york/ui/fill-picker/contexts/fill.ts",
      "type": "registry:lib",
      "target": "@ui/fill-picker/contexts/fill.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport type { FillPickerState } from \"../hooks/use-fill-picker\";\n\nexport const FillPickerContext = React.createContext<FillPickerState | null>(null);\n\n/**\n * Stable id prefix provided by <FillPicker.Root> so Tab (`aria-controls`)\n * and Pane (`aria-labelledby`) can reference each other across the tree.\n */\nexport const FillPickerIdContext = React.createContext<string>(\"fill-picker\");\n\nexport const fillTabId = (base: string, mode: string) => `${base}-tab-${mode}`;\nexport const fillPaneId = (base: string, mode: string) => `${base}-pane-${mode}`;\n\nexport function useFillPickerContext(): FillPickerState {\n  const ctx = React.useContext(FillPickerContext);\n  if (!ctx) {\n    throw new Error(\n      \"FillPicker.Tabs / Tab / Pane must be rendered inside <FillPicker.Root>\",\n    );\n  }\n  return ctx;\n}\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/hooks/use-fill-picker.ts",
      "type": "registry:hook",
      "target": "@ui/fill-picker/hooks/use-fill-picker.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  DEFAULT_LINEAR,\n  formatFill,\n  type Fill,\n  type ColorFill,\n  type GradientFill,\n} from \"../lib/gradient\";\nimport type { OklchColor } from \"../lib/types\";\n\nconst DEFAULT_COLOR: OklchColor = { l: 0, c: 0, h: 0, alpha: 1 };\n\nexport type FillMode = \"color\" | \"gradient\";\n\nexport interface UseFillPickerProps {\n  value?: Fill;\n  defaultValue?: Fill;\n  onValueChange?: (fill: Fill, css: string) => void;\n  mode?: FillMode;\n  defaultMode?: FillMode;\n  onModeChange?: (mode: FillMode) => void;\n}\n\nexport interface FillPickerState {\n  fill: Fill;\n  mode: FillMode;\n  setFill: (fill: Fill) => void;\n  setMode: (mode: FillMode) => void;\n}\n\nexport function useFillPicker(props: UseFillPickerProps = {}): FillPickerState {\n  const {\n    value,\n    defaultValue,\n    onValueChange,\n    mode: modeProp,\n    defaultMode,\n    onModeChange,\n  } = props;\n\n  // With no seed fill, the default fill follows the requested mode so\n  // `defaultMode: \"gradient\"` doesn't start on a color fill.\n  const seedFill = value ?? defaultValue;\n  const initialMode: FillMode =\n    modeProp ?? defaultMode ?? seedFill?.kind ?? \"color\";\n  const initialFill: Fill =\n    seedFill ??\n    (initialMode === \"gradient\"\n      ? { kind: \"gradient\", gradient: DEFAULT_LINEAR }\n      : { kind: \"color\", color: DEFAULT_COLOR });\n\n  const [internalFill, setInternalFill] = React.useState<Fill>(initialFill);\n  const [internalMode, setInternalMode] = React.useState<FillMode>(initialMode);\n\n  // Derive in render — don't sync props → state via useEffect (per React's\n  // \"you might not need an effect\" guidance). The controlled value is the\n  // source of truth when provided; internal state is only consulted when\n  // uncontrolled.\n  const isControlled = value !== undefined;\n  const isControlledMode = modeProp !== undefined;\n  const fill: Fill = isControlled ? value : internalFill;\n  const mode: FillMode = isControlledMode ? modeProp : internalMode;\n\n  // Track the last committed fill for each kind so setMode can restore the\n  // cached side. A layout effect updates the cache before input can arrive\n  // without mutating refs during render.\n  const lastColorRef = React.useRef<ColorFill>(\n    initialFill.kind === \"color\"\n      ? initialFill\n      : { kind: \"color\", color: DEFAULT_COLOR },\n  );\n  const lastGradientRef = React.useRef<GradientFill>(\n    initialFill.kind === \"gradient\"\n      ? initialFill\n      : { kind: \"gradient\", gradient: DEFAULT_LINEAR },\n  );\n  React.useLayoutEffect(() => {\n    if (fill.kind === \"color\") lastColorRef.current = fill;\n    else lastGradientRef.current = fill;\n  }, [fill]);\n\n  const setFill = React.useCallback(\n    (next: Fill) => {\n      if (!isControlled) setInternalFill(next);\n      onValueChange?.(next, formatFill(next));\n    },\n    [isControlled, onValueChange],\n  );\n\n  const setMode = React.useCallback(\n    (next: FillMode) => {\n      if (!isControlledMode) setInternalMode(next);\n      onModeChange?.(next);\n      const restored: Fill =\n        next === \"color\" ? lastColorRef.current : lastGradientRef.current;\n      if (!isControlled) setInternalFill(restored);\n      onValueChange?.(restored, formatFill(restored));\n    },\n    [isControlled, isControlledMode, onModeChange, onValueChange],\n  );\n\n  return { fill, mode, setFill, setMode };\n}\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/fill/root.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/fill/root.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { FillPickerContext, FillPickerIdContext } from \"../../contexts/fill\";\nimport {\n  useFillPicker,\n  type UseFillPickerProps,\n} from \"../../hooks/use-fill-picker\";\n\nexport interface RootProps\n  extends UseFillPickerProps,\n    Omit<React.HTMLAttributes<HTMLDivElement>, \"defaultValue\" | \"onChange\"> {}\n\nexport const Root = React.forwardRef<HTMLDivElement, RootProps>(function Root(\n  {\n    value,\n    defaultValue,\n    onValueChange,\n    mode,\n    defaultMode,\n    onModeChange,\n    className,\n    children,\n    ...rest\n  },\n  ref,\n) {\n  const state = useFillPicker({\n    value,\n    defaultValue,\n    onValueChange,\n    mode,\n    defaultMode,\n    onModeChange,\n  });\n\n  // Animate the Root's height as panes swap between Solid and Gradient\n  // (and as the gradient pane itself grows/shrinks with shape controls).\n  // Pure CSS `transition: height` can't animate to/from `auto`, so we\n  // observe the live content height via ResizeObserver and pin it as an\n  // inline pixel value — the transition takes care of the rest. No\n  // dependencies, no measurement pass: the observer fires the moment\n  // React commits the new tree.\n  const innerRef = React.useRef<HTMLDivElement | null>(null);\n  const [innerHeight, setInnerHeight] = React.useState<number | null>(null);\n  React.useLayoutEffect(() => {\n    const el = innerRef.current;\n    if (!el || typeof ResizeObserver === \"undefined\") return;\n    const ro = new ResizeObserver(([entry]) => {\n      // border-box so padding inside `innerRef` is included.\n      const box = entry.borderBoxSize?.[0];\n      setInnerHeight(box ? box.blockSize : entry.contentRect.height);\n    });\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, []);\n  // Skip the very first commit so the picker mounts at its natural\n  // height instead of animating up from 0.\n  const animateRef = React.useRef(false);\n  React.useEffect(() => {\n    if (innerHeight !== null) animateRef.current = true;\n  }, [innerHeight]);\n  const idBase = React.useId();\n  return (\n    <FillPickerContext.Provider value={state}>\n      <FillPickerIdContext.Provider value={idBase}>\n      <div\n        ref={ref}\n        data-slot=\"fill-picker\"\n        className={cn(\n          \"w-full max-w-70 overflow-hidden rounded-lg border border-border bg-popover text-popover-foreground shadow-sm\",\n          animateRef.current &&\n            \"transition-[height] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)]\",\n          className,\n        )}\n        {...rest}\n        style={\n          innerHeight !== null ? { height: innerHeight, ...rest.style } : rest.style\n        }\n      >\n        <div ref={innerRef} className=\"flex flex-col gap-2 p-3\">\n          {children}\n        </div>\n      </div>\n      </FillPickerIdContext.Provider>\n    </FillPickerContext.Provider>\n  );\n});\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/fill/tabs.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/fill/tabs.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  fillPaneId,\n  fillTabId,\n  FillPickerIdContext,\n  useFillPickerContext,\n} from \"../../contexts/fill\";\nimport type { FillMode } from \"../../hooks/use-fill-picker\";\n\nexport const Tabs = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(function Tabs({ className, children, ...rest }, ref) {\n  // APG Tabs pattern: Left/Right arrows rove between tabs and select as\n  // they go (selection follows focus — panes render instantly). Activating\n  // via .click() keeps any consumer onClick on the Tab in the loop.\n  const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n    const keys = [\"ArrowRight\", \"ArrowLeft\", \"Home\", \"End\"];\n    if (!keys.includes(e.key)) return;\n    const tabs = Array.from(\n      e.currentTarget.querySelectorAll<HTMLButtonElement>('[role=\"tab\"]'),\n    );\n    if (tabs.length === 0) return;\n    const current = tabs.indexOf(\n      document.activeElement as HTMLButtonElement,\n    );\n    let next: number;\n    if (e.key === \"Home\") next = 0;\n    else if (e.key === \"End\") next = tabs.length - 1;\n    else {\n      const delta = e.key === \"ArrowRight\" ? 1 : -1;\n      next = (Math.max(current, 0) + delta + tabs.length) % tabs.length;\n    }\n    e.preventDefault();\n    tabs[next].focus();\n    tabs[next].click();\n  };\n  return (\n    <div\n      ref={ref}\n      role=\"tablist\"\n      aria-label=\"Fill mode\"\n      onKeyDown={onKeyDown}\n      className={cn(\"inline-flex items-center gap-1 rounded-md bg-muted p-1\", className)}\n      {...rest}\n    >\n      {children}\n    </div>\n  );\n});\n\nexport interface TabProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  mode: FillMode;\n}\n\nexport const Tab = React.forwardRef<HTMLButtonElement, TabProps>(function Tab(\n  { mode, className, children, ...rest },\n  ref,\n) {\n  const ctx = useFillPickerContext();\n  const idBase = React.useContext(FillPickerIdContext);\n  const active = ctx.mode === mode;\n  return (\n    <button\n      ref={ref}\n      type=\"button\"\n      role=\"tab\"\n      id={fillTabId(idBase, mode)}\n      aria-controls={fillPaneId(idBase, mode)}\n      aria-selected={active}\n      tabIndex={active ? 0 : -1}\n      data-state={active ? \"active\" : \"inactive\"}\n      onClick={() => ctx.setMode(mode)}\n      className={cn(\n        \"rounded-sm px-3 py-1 text-xs font-medium outline-none transition-colors\",\n        \"focus-visible:ring-2 focus-visible:ring-ring\",\n        active\n          ? \"bg-background text-foreground shadow-sm\"\n          : \"text-muted-foreground hover:text-foreground\",\n        className,\n      )}\n      {...rest}\n    >\n      {children}\n    </button>\n  );\n});\n"
    },
    {
      "path": "registry/new-york/ui/fill-picker/parts/fill/pane.tsx",
      "type": "registry:ui",
      "target": "@ui/fill-picker/parts/fill/pane.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { ColorPickerContext } from \"../../context\";\nimport { GradientPickerContext } from \"../../contexts/gradient\";\nimport {\n  fillPaneId,\n  fillTabId,\n  FillPickerIdContext,\n  useFillPickerContext,\n} from \"../../contexts/fill\";\nimport { useColorPicker } from \"../../hooks/use-color-picker\";\nimport { useGradientPicker } from \"../../hooks/use-gradient-picker\";\nimport type { FillMode } from \"../../hooks/use-fill-picker\";\nimport { DEFAULT_LINEAR, type Gradient } from \"../../lib/gradient\";\nimport type { OklchColor } from \"../../lib/types\";\n\nexport interface PaneProps extends React.HTMLAttributes<HTMLDivElement> {\n  mode: FillMode;\n}\n\nexport const Pane = React.forwardRef<HTMLDivElement, PaneProps>(function Pane(\n  { mode, className, children, ...rest },\n  ref,\n) {\n  const fill = useFillPickerContext();\n  const idBase = React.useContext(FillPickerIdContext);\n  if (fill.mode !== mode) return null;\n\n  // APG tabpanel wiring: named by its tab, referenced by aria-controls.\n  const panelProps = {\n    role: \"tabpanel\" as const,\n    id: fillPaneId(idBase, mode),\n    \"aria-labelledby\": fillTabId(idBase, mode),\n  };\n\n  if (mode === \"color\") {\n    return (\n      <ColorPaneInner ref={ref} className={className} {...panelProps} {...rest}>\n        {children}\n      </ColorPaneInner>\n    );\n  }\n  return (\n    <GradientPaneInner ref={ref} className={className} {...panelProps} {...rest}>\n      {children}\n    </GradientPaneInner>\n  );\n});\n\n/**\n * Tiny mount-fade hook. Pane swaps unmount one tree and mount another;\n * starting at opacity 0 and flipping to 1 on the next frame gives a\n * dependency-free crossfade. Two RAFs because a single one can land in\n * the same browser tick as the React commit on some engines.\n */\nfunction useMountFade() {\n  const [visible, setVisible] = React.useState(false);\n  React.useEffect(() => {\n    let raf2 = 0;\n    const raf1 = requestAnimationFrame(() => {\n      raf2 = requestAnimationFrame(() => setVisible(true));\n    });\n    return () => {\n      cancelAnimationFrame(raf1);\n      cancelAnimationFrame(raf2);\n    };\n  }, []);\n  return visible;\n}\n\nconst ColorPaneInner = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(function ColorPaneInner({ className, children, ...rest }, ref) {\n  const fill = useFillPickerContext();\n  const colorValue: OklchColor =\n    fill.fill.kind === \"color\"\n      ? fill.fill.color\n      : { l: 0, c: 0, h: 0, alpha: 1 };\n\n  const setFill = fill.setFill;\n  const onValueChange = React.useCallback(\n    (color: OklchColor) => setFill({ kind: \"color\", color }),\n    [setFill],\n  );\n\n  const state = useColorPicker({ value: colorValue, onValueChange });\n  const visible = useMountFade();\n\n  return (\n    <ColorPickerContext.Provider value={state}>\n      <div\n        ref={ref}\n        data-slot=\"fill-picker-pane\"\n        data-mode=\"color\"\n        className={cn(\n          \"transition-opacity duration-300 ease-in\",\n          visible ? \"opacity-100\" : \"opacity-0\",\n          className,\n        )}\n        {...rest}\n      >\n        {children}\n      </div>\n    </ColorPickerContext.Provider>\n  );\n});\n\nconst GradientPaneInner = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(function GradientPaneInner({ className, children, ...rest }, ref) {\n  const fill = useFillPickerContext();\n  const gradientValue: Gradient =\n    fill.fill.kind === \"gradient\" ? fill.fill.gradient : DEFAULT_LINEAR;\n\n  const setFill = fill.setFill;\n  const onValueChange = React.useCallback(\n    (gradient: Gradient) => setFill({ kind: \"gradient\", gradient }),\n    [setFill],\n  );\n\n  const state = useGradientPicker({ value: gradientValue, onValueChange });\n  const visible = useMountFade();\n\n  return (\n    <GradientPickerContext.Provider value={state}>\n      <div\n        ref={ref}\n        data-slot=\"fill-picker-pane\"\n        data-mode=\"gradient\"\n        className={cn(\n          \"transition-opacity duration-300 ease-in\",\n          visible ? \"opacity-100\" : \"opacity-0\",\n          className,\n        )}\n        {...rest}\n      >\n        {children}\n      </div>\n    </GradientPickerContext.Provider>\n  );\n});\n"
    }
  ]
}
